Learning Platform
Глоссарий Troubleshooting
Урок 08.01 · 22 мин
Средний
Type hintsPEP 585PEP 604list[int]dict[str, int]int | Nonetypes.UnionTypeModern syntax

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).

В этом уроке:

  1. Why — зачем type hints в pragmatic коде.
  2. PEP 585 built-in generics — list[int], dict[str, list[int]], tuple[int, ...], set[str].
  3. PEP 604 unions — int | str, int | None.
  4. types.UnionType — runtime класс для int | None (empirically).
  5. Deprecated formsList / Optional (импортированные из typing) забыть навсегда; replacement table.
  6. 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’у:

  1. Signature как документация: (s: str) -> int | None — не нужен docstring чтобы понять, что вернёт None на invalid.
  2. mypy проверит, что caller обрабатывает None: age = parse_age(s); age + 1 поднимает error: Unsupported operand types for + ("None" and "int").
  3. IDE autocomplete: после age = parse_age(s) IDE предлагает age.bit_length, age.real и т.д. с возможностью Noneage + 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-inGeneric formComment
listlist[T]Mutable sequence, homogeneous (по convention)
dictdict[K, V]Hash table
setset[T]Hash set
frozensetfrozenset[T]Immutable set
tupletuple[T1, T2, T3]Heterogeneous fixed-length ИЛИ tuple[T, ...] для variable-length homogeneous
typetype[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.pyUnionType (документируется с Python 3.10).

PEP 585/604 — modern type-hint vocabulary
list[int]PEP 585 genericlist.__class_getitem__(int) возвращает types.GenericAlias instance с __origin__=list, __args__=(int,). mypy интерпретирует как 'list of ints'. Не runtime-checked: list[int] не проверяет что элементы int — это статическая аннотация для type checker'ов
dict[str, V]key/value parametricdict.__class_getitem__((str, V)) — два аргумента: key type и value type. __origin__=dict, __args__=(str, V). Standard idiom для 'JSON-like' структур: dict[str, int|str|list]
int | NonePEP 604 unionint.__or__(None) (или type.__or__) возвращает types.UnionType instance. __args__=(int, NoneType). isinstance(x, int | None) работает в runtime. Заменяет старую Optional[int] форму (импортируемую из typing) полностью — short, idiomatic
tuple[T, ...]variable-length homogeneousЭллипсис (`...`) — special form означающая 'arbitrary length'. tuple[int, ...] = 'tuple любой длины из int'. Без эллипсиса: tuple[int, str] — fixed-length 2-tuple (heterogeneous). M02 урок 06: immutable handle, hashable, безопасно использовать как dict key

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)ReplacementPEP
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 | Y604
typing :: Optional[X]X | None604
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-коде.


В 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.


Ключевые выводы

  1. PEP 585 (Python 3.9, 2020) — list[int], dict[K, V], tuple[T, ...], set[T], type[C] — built-in generics через __class_getitem__types.GenericAlias.
  2. PEP 604 (Python 3.10, 2021) — union via |: int | str, int | None. Runtime — types.UnionType instance, isinstance(x, int | str) работает.
  3. X | None заменяет Optional[X] полностью. X | Y заменяет Union[X, Y] полностью.
  4. 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.
  5. type[C] для класс-как-значение: def make(cls: type[Foo]) -> Foo:. Часто полезно для factory functions.
  6. Cross-link M02 урок 06: tuple[int, ...] typed immutable hashable handle — расширение pattern’а, заложенного в M02.
  7. 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.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какой PEP вводит синтаксис `list[int]` (built-in generics) — без `from typing import List`?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7