Modern type hints: PEP 585 / PEP 604
Type hints в Python 3.13 — это first-class инструмент дизайна API. Они не runtime-checked (это не Java и не TypeScript-strict), но они: (1) документируют контракт функции в самой signature, (2) позволяют mypy / pyright / pyre статически проверить корректность, (3) питают IDE autocomplete’ом — и (4) позволяют runtime-introspection через typing.get_type_hints (см. урок 04). В этом уроке закладываем базовый синтаксический baseline всего модуля: built-in generics (PEP 585) и union types (PEP 604).
В этом уроке:
- Why — зачем type hints в pragmatic коде.
- PEP 585 built-in generics —
list[int],dict[str, list[int]],tuple[int, ...],set[str]. - PEP 604 unions —
int | str,int | None. types.UnionType— runtime класс дляint | None(empirically).- Deprecated forms —
List/Optional(импортированные изtyping) забыть навсегда; replacement table. - Cross-link M02 урок 06 —
tuple[int, ...]как immutable hashable handle.
Это invariant курса с этого момента: все code-блоки в M07/M08 используют только modern syntax. Wave 0 lint validate-no-deprecated-typing.cjs (Plan 67-01) автоматически блокирует regressions.
Why type hints — recipe parse_age
Простая utility функция парсит строку в возраст или возвращает None на invalid input:
def parse_age(s: str) -> int | None:
"""Parse age string. Return int if valid, None otherwise."""
try:
n = int(s)
except ValueError:
return None
return n if 0 <= n <= 150 else None
print(parse_age('42')) # 42
print(parse_age('abc')) # None
print(parse_age('-5')) # None (out of range)
print(parse_age('200')) # None (out of range)
Что type hints дают caller’у:
- Signature как документация:
(s: str) -> int | None— не нужен docstring чтобы понять, что вернётNoneна invalid. - mypy проверит, что caller обрабатывает
None:age = parse_age(s); age + 1поднимаетerror: Unsupported operand types for + ("None" and "int"). - IDE autocomplete: после
age = parse_age(s)IDE предлагаетage.bit_length,age.realи т.д. с возможностьюNone—age + 1подсветится warning’ом, пока не сделаешь narrow (if age is not None: ...— см. урок 05).
Pragmatic-DEEP rule: каждая public функция в production-коде должна иметь типы аргументов и return type. Без них ваш код — не документирован, не валидируем mypy, не completion-friendly.
PEP 585 — built-in generics
До Python 3.9 для аннотации “list of ints” приходилось импортировать List из typing модуля и писать List[int]. С PEP 585 (Python 3.9, 2020) built-in типы стали generic’ами напрямую — list[int], dict[str, V], tuple[T, ...], set[T]:
def first_evens(nums: list[int]) -> list[int]:
"""Return first 5 even numbers from list."""
return [n for n in nums if n % 2 == 0][:5]
def index_by_id(items: list[dict[str, int]]) -> dict[int, dict[str, int]]:
"""Index list of records by 'id' field."""
return {item['id']: item for item in items}
def stats(values: tuple[int, ...]) -> dict[str, float]:
"""Variable-length tuple of ints (homogeneous)."""
return {'mean': sum(values) / len(values), 'max': float(max(values))}
def unique_tags(records: list[dict[str, list[str]]]) -> set[str]:
"""Collect all tags across nested records."""
out: set[str] = set()
for r in records:
out.update(r.get('tags', []))
return out
Какие типы стали generic’ами в PEP 585 (полный список):
| Built-in | Generic form | Comment |
|---|---|---|
list | list[T] | Mutable sequence, homogeneous (по convention) |
dict | dict[K, V] | Hash table |
set | set[T] | Hash set |
frozenset | frozenset[T] | Immutable set |
tuple | tuple[T1, T2, T3] | Heterogeneous fixed-length ИЛИ tuple[T, ...] для variable-length homogeneous |
type | type[C] | Класс-как-значение (def factory(cls: type[Foo]) -> Foo:) |
Это работает потому что PEP 585 добавил __class_getitem__ методу всем этим типам: list[int] буквально equivalent list.__class_getitem__(int) — возвращает types.GenericAlias instance, который mypy интерпретирует как parametric type.
print(list[int]) # list[int]
print(type(list[int])) # <class 'types.GenericAlias'>
print(list[int].__origin__) # <class 'list'>
print(list[int].__args__) # (<class 'int'>,)
Cite: Lib/types.py — класс GenericAlias (актуально wrapper над C-типом Py_GenericAlias из Objects/genericaliasobject.c).
PEP 604 — union types X | Y
До Python 3.10 для union приходилось импортировать Union или Optional из typing модуля и писать Union[int, str] или Optional[int]. С PEP 604 (Python 3.10, 2021) синтаксис стал inline:
def parse_int_or_keep(x: int | str) -> int | str:
"""If x is str digits — convert to int; else return as-is."""
if isinstance(x, str) and x.isdigit():
return int(x)
return x
def maybe_user(uid: int) -> dict[str, int | str] | None:
"""Lookup user; return None if missing."""
return {'id': uid, 'name': 'alice'} if uid > 0 else None
def normalize(items: list[int | str]) -> list[str]:
"""Normalize mixed int/str list to all-str."""
return [str(x) for x in items]
Правило X | None заменяет Optional[X]. Это тот же тип semantically (PEP 604 explicit это), но синтаксис чище:
# OK — PEP 604:
def find(uid: int) -> dict | None: ...
WRONG (deprecated, blocked Wave 0 lint): импорт Optional или Union из typing модуля + использование Optional[dict]/Union[int, str] в signature. Modern эквивалент — dict | None / int | str напрямую.
В M07/M08 только PEP 604 форма. Wave 0 lint validate-no-deprecated-typing.cjs (Plan 67-01) блокирует deprecated формы automatically.
types.UnionType — runtime class
Когда вы пишете int | None, Python создаёт runtime instance класса types.UnionType. Это не просто аннотация-строка — это объект с runtime semantics:
import types
t = int | None
print(t) # int | None
print(type(t)) # <class 'types.UnionType'>
print(t.__args__) # (<class 'int'>, <class 'NoneType'>)
# isinstance работает с UnionType (Python 3.10+)
print(isinstance(42, int | str)) # True
print(isinstance('hello', int | str)) # True
print(isinstance(3.14, int | str)) # False
# isinstance с | None (== Optional)
print(isinstance(None, int | None)) # True
print(isinstance(42, int | None)) # True
Это enables runtime narrowing patterns (см. урок 05 — isinstance(x, int | str) narrows к int | str). Cite: Lib/types.py — UnionType (документируется с Python 3.10).
Deprecated forms — replacement table
Wave 0 lint validate-no-deprecated-typing.cjs (Plan 67-01) блокирует все эти формы в M07/M08 paths. Они не deprecated в Python (всё ещё работают для backward-compat), но deprecated в стиле курса — modern syntax invariant.
Запись typing :: X ниже — обозначение deprecated формы (импорт из typing модуля); запись справа — modern замена. Лесонные code-блоки никогда не используют deprecated форму.
| Deprecated (FORBIDDEN) | Replacement | PEP |
|---|---|---|
typing :: List[T] | list[T] | 585 |
typing :: Dict[K, V] | dict[K, V] | 585 |
typing :: Set[T] | set[T] | 585 |
typing :: FrozenSet[T] | frozenset[T] | 585 |
typing :: Tuple[T1, T2] | tuple[T1, T2] | 585 |
typing :: Tuple[T, ...] | tuple[T, ...] | 585 |
typing :: Type[C] | type[C] | 585 |
typing :: Union[X, Y] | X | Y | 604 |
typing :: Optional[X] | X | None | 604 |
typing :: Callable[[A, B], R] | collections.abc.Callable[[A, B], R] | 585 |
typing :: Iterable[T] | collections.abc.Iterable[T] | 585 |
typing :: Iterator[T] | collections.abc.Iterator[T] | 585 |
typing :: Sequence[T] | collections.abc.Sequence[T] | 585 |
typing :: Mapping[K, V] | collections.abc.Mapping[K, V] | 585 |
typing :: MutableMapping[K, V] | collections.abc.MutableMapping[K, V] | 585 |
Что остаётся в typing модуле и разрешено: Self (PEP 673), Protocol (PEP 544), TypedDict (PEP 589), TypeVar, Generic, cast, get_type_hints, TypeGuard (PEP 647), Final, ClassVar, LiteralString, Required / NotRequired (PEP 655), Annotated (PEP 593). Эти формы не имеют built-in замены — from typing import Protocol, TypedDict, TypeVar, Self, ... остаётся в production-коде.
Cross-link M02 урок 06 — tuple[int, ...] immutable handle
В M02 урок 06 мы установили: tuple immutable + hashable → стандартный pattern для resource handle или dict key. С PEP 585 теперь его можно типизировать:
# M02 урок 06: координаты как dict key (hashable tuple)
distances: dict[tuple[int, int], float] = {
(0, 0): 0.0,
(1, 0): 1.0,
(0, 1): 1.0,
(1, 1): 1.41,
}
# tuple[int, ...] — homogeneous, variable length
def average(values: tuple[int, ...]) -> float:
"""Average of variable-length int tuple."""
return sum(values) / len(values)
print(average((1, 2, 3, 4, 5))) # 3.0
print(average((10, 20))) # 15.0
# tuple[int, str, float] — heterogeneous, fixed length
def parse_record(rec: tuple[int, str, float]) -> str:
"""Format fixed 3-tuple record."""
uid, name, score = rec
return f'{name} (#{uid}): {score}'
print(parse_record((42, 'alice', 0.95))) # 'alice (#42): 0.95'
tuple[int, ...] (с эллипсисом) — variable-length, все элементы одного типа. tuple[int, str, float] (без эллипсиса) — fixed-length, разные типы по позициям. Это связь с M02 урок 06: immutable handle = безопасный hashable key, теперь с явным typed-контрактом.
Cite: PEP 585 Section “Implementation” — built-in tuple получил __class_getitem__ через types.GenericAlias infrastructure.
Recipe: typed config loader
Производственный pattern — функция читает JSON-config (через json.load) и валидирует структуру через type hints + runtime check:
import json
import io
def load_config(source: str) -> dict[str, int | str | bool]:
"""Parse JSON config string. Top-level dict с scalar values (int/str/bool)."""
raw = json.loads(source)
if not isinstance(raw, dict):
raise ValueError(f'expected dict, got {type(raw).__name__}')
out: dict[str, int | str | bool] = {}
for k, v in raw.items():
if not isinstance(k, str):
raise ValueError(f'expected str key, got {type(k).__name__}')
if isinstance(v, (int, str, bool)) and not isinstance(v, float):
out[k] = v
else:
raise ValueError(f'expected int|str|bool value for {k!r}, got {type(v).__name__}')
return out
cfg = load_config('{"port": 8080, "host": "localhost", "debug": true}')
print(cfg) # {'port': 8080, 'host': 'localhost', 'debug': True}
print(cfg['port'] + 1) # 8081 — mypy знает что port: int|str|bool, нужен narrow
Type hints здесь — контракт для caller’а: “функция вернёт dict[str, int|str|bool]; если входные данные нарушают, raise ValueError”. Это не runtime type checking (типы не enforced’ятся CPython), но это документация + mypy + IDE friendly.
Ключевые выводы
- PEP 585 (Python 3.9, 2020) —
list[int],dict[K, V],tuple[T, ...],set[T],type[C]— built-in generics через__class_getitem__→types.GenericAlias. - PEP 604 (Python 3.10, 2021) — union via
|:int | str,int | None. Runtime —types.UnionTypeinstance,isinstance(x, int | str)работает. X | NoneзаменяетOptional[X]полностью.X | YзаменяетUnion[X, Y]полностью.- Deprecated в стиле курса: формы
List/Dict/Optional/Union/Callableимпортируемые изtypingмодуля и т.д. (12 forms) — замена вlist[T]/dict[K,V]/X | None/X | Y/collections.abc.Callable. Wave 0 lint блокирует automatically. type[C]для класс-как-значение:def make(cls: type[Foo]) -> Foo:. Часто полезно для factory functions.- Cross-link M02 урок 06:
tuple[int, ...]typed immutable hashable handle — расширение pattern’а, заложенного в M02. - Type hints = pragmatic-DEEP baseline: signature как документация + mypy support + IDE autocomplete + runtime introspection (урок 04). Каждая public функция в production-коде должна иметь полные types.
Дальше — урок 02 — generic функции и классы через PEP 695 (class Stack[T]:, def first[T](items: list[T]) -> T:) и legacy-bridge через TypeVar.