Learning Platform
Глоссарий Troubleshooting
Урок 08.02 · 25 мин
Средний
GenericsPEP 695PEP 696TypeVarGeneric[T]Type aliasParamSpecVarianceModern syntaxType defaults
Требуемые знания:

Generic functions и classes — PEP 695 modern syntax

В уроке 01 мы аннотировали конкретные типы: list[int], dict[str, int], int | None. Но что если функция работает с любым типом? def first(items: list) — теряет информацию (return type — Any). Нужны generic functions — функции параметризованные типом: def first[T](items: list[T]) -> T: — “если получили list of T, вернём T”.

PEP 695 (Python 3.12, 2023) дал inline syntax для generics — class Stack[T]:, def first[T](...), type Vector = list[float]. До PEP 695 (Python ≤ 3.11) приходилось импортировать TypeVar и наследовать Generic[T] — verbose. Modern PEP 695 syntax — default в M07/M08 курса.

В этом уроке:

  1. PEP 695 generic functionsdef first[T](items: list[T]) -> T:.
  2. PEP 695 generic classesclass Stack[T]:.
  3. PEP 695 type aliasestype Vector = list[float], type JSON = ....
  4. Legacy bridgeTypeVar('T') + Generic[T] для existing codebases.
  5. Variance — covariance / contravariance (recipe-level).
  6. ParamSpec — typed decorators preserve signature; cross-link M03 урок 04 (closure carries arguments через *args, **kwargs).

PEP 695 generic function — def first[T]

Простейший generic — функция возвращающая первый элемент списка с сохранением типа:

def first[T](items: list[T]) -> T:
    """Return first element. Raises IndexError if empty."""
    return items[0]


# T inferred mypy'ом по call site
ints: list[int] = [1, 2, 3]
strs: list[str] = ['a', 'b', 'c']

x: int = first(ints)         # mypy знает: T = int → x: int
y: str = first(strs)         # mypy знает: T = str → y: str

Что произошло на уровне Python:

  1. [T] объявляет type parameter T для функции first. Это scope-level аннотация, action — на parser-уровне, не runtime (PEP 695 Section “Generic Functions”).
  2. items: list[T] — параметр типа list-of-T (T resolved per-call mypy’ом).
  3. -> T — return type same T.
  4. mypy / pyright при call first(ints) substitutes T = int — return type становится int.

В runtime T — instance typing.TypeVar (создан implicitly из PEP 695 syntax). first.__type_params__ — кортеж type parameters:

print(first.__type_params__)            # (T,)
print(first.__type_params__[0])         # T
print(type(first.__type_params__[0]))   # <class 'typing.TypeVar'>

Cite: Lib/typing.pyTypeVar class; PEP 695 описывает как compiler def fn[T](...) синтаксис desugars в TypeVar creation.


PEP 695 generic class — class Stack[T]

Generic класс — параметризованный data structure. Stack-of-T:

class Stack[T]:
    """Generic LIFO stack."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

    def peek(self) -> T | None:
        return self._items[-1] if self._items else None

    def __len__(self) -> int:
        return len(self._items)


# Использование — T resolved при construction
ints = Stack[int]()
ints.push(1)
ints.push(2)
print(ints.pop())          # 2 — mypy знает: int

strs = Stack[str]()
strs.push('hello')
print(strs.peek())         # 'hello' — mypy знает: str | None

# Type error при неправильном типе:
# ints.push('oops')  # mypy: Argument 1 has incompatible type "str"; expected "int"

Что compiler делает в Python 3.12+:

  1. class Stack[T]: — добавляет type parameter T в class scope.
  2. Stack.__type_params__ = (T,) — runtime accessible.
  3. Stack[int]() — сначала Stack.__class_getitem__(int) создаёт types.GenericAlias (как в PEP 585), потом () инстанциирует.
  4. mypy разрезает signature Stack[int].push(self, item: T)(self, item: int) substitution.
print(Stack.__type_params__)        # (T,)
print(Stack[int])                   # __main__.Stack[int]
print(type(Stack[int]))             # <class 'types.GenericAlias'>

PEP 695 type alias — type Vector = list[float]

PEP 695 ввёл новый statement type для алиасов:

type Vector = list[float]
type Matrix = list[list[float]]
type JSON = int | str | bool | None | list['JSON'] | dict[str, 'JSON']


def magnitude(v: Vector) -> float:
    """Euclidean norm."""
    return sum(x * x for x in v) ** 0.5


def transpose(m: Matrix) -> Matrix:
    """Transpose square matrix."""
    return [[m[j][i] for j in range(len(m))] for i in range(len(m[0]))]


print(magnitude([3.0, 4.0]))                       # 5.0
print(transpose([[1.0, 2.0], [3.0, 4.0]]))         # [[1.0, 3.0], [2.0, 4.0]]

type statement vs обычное присваивание:

# OK — PEP 695:
type Vec1 = list[float]
print(type(Vec1))           # <class 'typing.TypeAliasType'>

# Старый стиль (всё ещё работает, но не recommended):
Vec2 = list[float]
print(type(Vec2))           # <class 'types.GenericAlias'> — не distinct alias

PEP 695 type создаёт typing.TypeAliasType instance — мypy и pyright treat это как named alias (better error messages, recursive types работают). Cite: Lib/typing.pyTypeAliasType class.


Legacy bridge — TypeVar + Generic

В существующих codebases (Python 3.11 и старше) generics писали через TypeVar + Generic[T]:

# Legacy (Python 3.11 и старше):
from typing import TypeVar, Generic

T = TypeVar('T')


def first_legacy(items: list[T]) -> T:
    return items[0]


class Stack_legacy(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, item: T) -> None:
        self._items.append(item)
    def pop(self) -> T:
        return self._items.pop()

Эквивалентно PEP 695 syntax. Для новых проектов используйте PEP 695 (компактнее, читабельнее). Для maintenance существующих codebases — может быть удобно остаться на legacy syntax (consistency).

Bound TypeVars — type parameter ограничен subclass’ом:

# Legacy:
from typing import TypeVar

NumT = TypeVar('NumT', bound=int | float)

def double_legacy(x: NumT) -> NumT:
    return x * 2

# PEP 695 modern:
def double[T: int | float](x: T) -> T:
    return x * 2


print(double(5))         # 10 — T inferred as int
print(double(3.14))      # 6.28 — T inferred as float
# double('hello')        # mypy error: incompatible bound

T: int | float (PEP 695) ≡ TypeVar('T', bound=int | float) (legacy) — type parameter может быть int, float, или их subclass.


Variance — covariant / contravariant

Variance — направление subtyping для generic types. Это deep типовая теория, но на practice одного recipe достаточно:

# Covariant T_co — read-only contexts (output positions)
class ImmutableList[T_co]:
    """Read-only list — covariant в T."""

    def __init__(self, items: list[T_co]) -> None:
        self._items: list[T_co] = list(items)

    def __getitem__(self, idx: int) -> T_co:
        return self._items[idx]

    def __len__(self) -> int:
        return len(self._items)


# Contravariant T_contra — write-only contexts (input positions)
class Sink[T_contra]:
    """Write-only sink — contravariant в T."""

    def write(self, item: T_contra) -> None:
        print(f'received: {item}')

Pragmatic правило:

  • Covariant (T_co) — type parameter появляется только в return positions (output). Pattern для read-only, immutable types. ImmutableList[Cat] — subtype ImmutableList[Animal] (если Cat — subtype Animal).
  • Contravariant (T_contra) — type parameter появляется только в parameter positions (input). Pattern для callbacks, sinks. Sink[Animal] — subtype Sink[Cat] (callback принимающий Animal принимает и Cat).
  • Invariant (default) — type parameter в обоих positions. list[T] invariant — list[Cat] НЕ subtype list[Animal] (mutable — push Animal в list[Cat] сломает type safety).

В PEP 695 variance специфицируется через suffix conventions на TypeVar (covariant T_co, contravariant T_contra) — mypy проверяет что usage consistent с declared variance. Cite: PEP 484 Section “Covariance and contravariance” — original treatment; PEP 695 carries semantics.

Для прикладного кода 90% времени достаточно invariant (default) — variance becomes important когда вы пишете library APIs с inheritance hierarchies.


ParamSpec — typed decorators preserve signature

Pitfall в декораторах из M06: wrapper’ы теряют signature ((*args, **kwargs) вместо оригинальной сигнатуры). Тогда mypy теряет проверку — caller декорированной функции может передать неправильные типы. Решение — ParamSpec (PEP 612, Python 3.10):

from collections.abc import Callable
import functools


def log_calls[**P, R](fn: Callable[P, R]) -> Callable[P, R]:
    """Decorator preserving signature через ParamSpec."""

    @functools.wraps(fn)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f'calling {fn.__name__} args={args}, kwargs={kwargs}')
        return fn(*args, **kwargs)

    return wrapper


@log_calls
def add(x: int, y: int) -> int:
    return x + y


# mypy preserves signature: add(x: int, y: int) -> int
result: int = add(1, 2)        # OK
# add('a', 'b')                # mypy error: incompatible types
# add(1)                       # mypy error: missing argument 'y'

Что нового в PEP 695 syntax:

  • [**P, R] — параметризация: **P — ParamSpec (capture’ит signature args/kwargs), R — обычный TypeVar для return type.
  • Callable[P, R] — function принимающая P.args, **P.kwargs (всю signature) и возвращающая R.
  • *args: P.args, **kwargs: P.kwargs — explicit forwarding с типами.

Cross-link M03 урок 04 (closure): wrapper — closure замыкающая fn через cell. M03 урок 04 показал как wrapper.__closure__[0].cell_contents is оригинал. ParamSpec — type-system overlay поверх closure mechanism: closure carries *args/**kwargs в runtime, ParamSpec обеспечивает type-safety на этом forwarding.

# Эмпирически verified (Python 3.12.7):
print(add.__name__)              # 'add'  (благодаря @functools.wraps)
print(add.__wrapped__)           # <function add at ...> (оригинал)
print(add.__type_params__)       # ()  (decorator's type params не попадают)

Cite: PEP 612 — Parameter Specification Variables; Lib/typing.pyParamSpec class.


PEP 696 — type parameter defaults (Python 3.13+)

PEP 696 — Type Defaults for Type Parameters (accepted, реализован в Python 3.13, October 2024) добавил default values для type parameters. До PEP 696 type parameter без явной аннотации со стороны caller’а превращался в Any (или Unknown в pyright). С 3.13+ можно задать default — class Container[T = int]: — если caller не указал тип, T = int.

# PEP 696 — Python 3.13+ syntax
class Container[T = int]:
    """Container с default type parameter — T = int если не указан явно."""

    def __init__(self, item: T) -> None:
        self.item: T = item

    def get(self) -> T:
        return self.item


# Без явного [int] — T defaults к int (PEP 696)
c1 = Container(42)              # mypy: Container[int]
print(c1.get())                  # 42

# С явным параметром — overrides default
c2: Container[str] = Container('hello')   # T = str
print(c2.get())                  # 'hello'

Что было до PEP 696 (Python ≤ 3.12 — TypeVar не имел default):

# Python 3.12 — без default, нужен explicit annotation
from typing import TypeVar, Generic

T = TypeVar('T')   # default не поддерживается в 3.12

class Container_legacy(Generic[T]):
    def __init__(self, item: T) -> None:
        self.item: T = item

# Без annotation — T = Unknown / Any
c = Container_legacy(42)         # mypy: Container_legacy[<nothing>] — теряется типизация

Generic functions с defaults — рабочая комбинация PEP 695 + PEP 696:

def first_or_default[T = int](items: list[T], default: T) -> T:
    """Возвращает первый элемент или default. T defaults к int."""
    return items[0] if items else default


# Caller без явного [T]:
x: int = first_or_default([1, 2, 3], 0)              # T inferred как int
y: str = first_or_default(['a', 'b'], 'fallback')    # T inferred как str

Comparison с TypeVar(default=...) (PEP 696 legacy form, тоже работает в 3.13+):

# Legacy syntax (3.13+ only — TypeVar получил параметр default):
from typing import TypeVar, Generic

T_legacy = TypeVar('T_legacy', default=int)

class Container_legacy_default(Generic[T_legacy]):
    def __init__(self, item: T_legacy) -> None:
        self.item: T_legacy = item

# Same semantics что PEP 695 [T = int]:
c = Container_legacy_default(42)        # T_legacy = int (default)

TypeVar default ranges — default может быть constrained: T = int | str, или ссылаться на bound:

# Default + bound:
class Numeric[T: int | float = float]:
    """Numeric box — T bounded к числам, defaults к float."""

    def __init__(self, value: T) -> None:
        self.value: T = value


n1 = Numeric(3.14)              # T = float (default match'нулся)
n2: Numeric[int] = Numeric(42)  # T = int (override)

Production use case: library API с reasonable default — caller не должен thinking about generic parameter в простых случаях, но может override для advanced patterns. Cite PEP 696 Section “Specification” + Python 3.13 What’s New — typing.

Pitfall: PEP 696 defaults — Python 3.13+. На 3.12 syntax class Container[T = int]: приведёт к SyntaxError. mypy/pyright tooling уже supports defaults в 3.13+ codebase; для compat с 3.12 — оставайтесь без defaults или используйте from __future__ import patterns.


Diagram: PEP 695 vs legacy generic syntax

PEP 695 modern vs TypeVar legacy
def first[T](items: list[T]) -> T:PEP 695 inlineCompiler в Python 3.12+ парсит [T] как scope-level type parameter. Создаёт TypeVar implicitly, добавляет в __type_params__. Никаких импортов. Один-в-один TypeVar('T') legacy, но без boilerplate. Cite PEP 695 Section 'Generic Functions'.
T = TypeVar('T'); def first(items: list[T]) -> T:Legacy explicit TypeVarPython ≤ 3.11 syntax. TypeVar('T') создаёт явный typing.TypeVar instance в module scope. Generic[T] inheritance для классов. Совместим с PEP 695 — оба производят TypeVar runtime objects, mypy treat'ит одинаково. Используется для maintenance existing codebases.

Recipe: typed retry decorator (PEP 695 + ParamSpec)

End-to-end production pattern — @retry декоратор с сохранением signature через ParamSpec:

from collections.abc import Callable
import functools


def retry[**P, R](max_attempts: int = 3) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Retry decorator factory — preserves signature через ParamSpec."""

    def decorator(fn: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(fn)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            last_exc: Exception | None = None
            for attempt in range(max_attempts):
                try:
                    return fn(*args, **kwargs)
                except Exception as e:
                    last_exc = e
            assert last_exc is not None
            raise last_exc
        return wrapper
    return decorator


@retry(max_attempts=3)
def flaky_compute(x: int, y: int) -> int:
    """Может бросить, попытается до 3 раз."""
    return x * y


# mypy preserves signature: flaky_compute(x: int, y: int) -> int
result: int = flaky_compute(3, 4)        # OK
print(result)                            # 12

@retry — three nested defs (M06 урок 02 pattern), теперь typed. **P, R — type params factory, не consumed на factory call (retry(max_attempts=3)); они resolve’ятся на decorator application (@retry(...) def flaky). mypy полностью типизированно — caller flaky_compute('a', 'b') поднимет error.


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

  1. PEP 695 (Python 3.12, 2023) — inline generic syntax: class Stack[T]:, def first[T](...), type Vector = list[float]. Default в M07/M08.
  2. __type_params__ — runtime tuple type parameters (атрибут на class / def / type). Empirically inspectable.
  3. Legacy bridgeTypeVar('T') + Generic[T] всё ещё работает; используется для existing codebases. PEP 695 эквивалент функционально.
  4. Bound TypeVarsdef double[T: int | float](x: T) (PEP 695) ≡ TypeVar('T', bound=int | float) (legacy).
  5. Variance: covariant T_co (output, immutable), contravariant T_contra (input, sinks), invariant default (mutable). 90% production-кода — invariant default.
  6. ParamSpec (PEP 612) — [**P, R] + Callable[P, R] + *args: P.args, **kwargs: P.kwargs для typed decorators preserving signature. Cross-link M03 урок 04: closure carries *args/**kwargs, ParamSpec — type-system overlay поверх этого.
  7. type Vector = list[float] — PEP 695 statement создаёт typing.TypeAliasType instance. Distinct alias (recursive types работают, better mypy error messages).
  8. PEP 696 type defaults (Python 3.13+) — class Container[T = int]: / def fn[T = int](...) / TypeVar('T', default=int). Если caller не указал параметр явно — используется default; иначе — overrides. Реальная type-safe alternative для T = Any fallback в legacy code.

Дальше — урок 03Protocol (structural subtyping) и TypedDict (typed dict-as-record).

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какой PEP **678** дал inline generic syntax `class Stack[T]:` (без явного `TypeVar` + `Generic`)?

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

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

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

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