Урок по треку Python 7 мин

Типизация в Python: аннотации, mypy, дженерики и Protocol

Аннотации, mypy, Optional и объединения, дженерики нового синтаксиса, Protocol, TypedDict.

Аннотации типов стали нормой в коммерческом Python: на них держатся FastAPI, Pydantic, SQLAlchemy 2.0, автодополнение в IDE и проверка в CI. На собесе спрашивают, что аннотации дают и чего не дают, и как описать нетривиальный тип.

Аннотации не проверяются в рантайме

def greet(name: str) -> str:
    return 'Привет, ' + name

greet(42)   # интерпретатор не возражает, упадёт только на сложении

Python хранит аннотации в __annotations__, но сам их не проверяет. Проверяют статические анализаторы: mypy, pyright, а в последнее время и быстрые проверяльщики на Rust. Библиотеки вроде Pydantic читают аннотации и валидируют данные сами.

Базовый синтаксис (3.10+)

def find(users: list[User], user_id: int) -> User | None: ...
config: dict[str, int | str] = {}
pair: tuple[int, str]
many: tuple[int, ...]        # кортеж любой длины
callback: Callable[[int, str], bool]
  • Встроенные коллекции параметризуются напрямую: list[int] (3.9+), без typing.List.
  • X | None (3.10+) то же, что Optional[X]. Optional значит «может быть None», а не «необязательный аргумент».
  • Any отключает проверку. object наоборот: принимает всё, но ничего не разрешает без сужения.

Сужение типов

def size(x: int | str) -> int:
    if isinstance(x, str):
        return len(x)     # здесь x: str
    return x              # здесь x: int

Анализатор понимает isinstance, is None, in, match. Для своих проверок есть TypeGuard и TypeIs (3.13).

Дженерики

С Python 3.12 (PEP 695) появился короткий синтаксис:

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

class Stack[T]:
    def __init__(self) -> None:
        self.items: list[T] = []

type Json = dict[str, 'Json'] | list['Json'] | str | int | float | bool | None

Раньше то же писали через T = TypeVar('T') и class Stack(Generic[T]), этот стиль ещё встречается в коде.

Protocol: утиная типизация для анализатора

from typing import Protocol

class Closable(Protocol):
    def close(self) -> None: ...

def shutdown(items: list[Closable]) -> None:
    for x in items:
        x.close()

Любой класс с методом close() подходит, наследоваться от Closable не нужно (структурная типизация). Это отличие от abc.ABC, где нужна явная регистрация или наследование.

TypedDict, Literal, Final и другие

  • TypedDict: словарь с известными ключами и типами значений, например JSON из API.
  • Literal['asc', 'desc']: только эти значения.
  • Final: константа, переприсваивание запрещено.
  • @overload: несколько сигнатур одной функции.
  • ParamSpec: типизация декораторов, сохраняющих сигнатуру.
  • Self (3.11): метод возвращает экземпляр своего класса.

Аннотации и время выполнения

Аннотации раньше вычислялись при определении функции, поэтому ссылка на ещё не объявленный класс требовала строки 'User' или from __future__ import annotations. В 3.14 аннотации вычисляются лениво (PEP 649), и прямые ссылки вперёд работают без кавычек. Для циклических импортов только ради типов используют if TYPE_CHECKING:.

Частые ошибки

  • Ожидание, что аннотации проверяются при вызове.
  • Optional[int] в смысле «аргумент можно не передавать».
  • Any повсюду: проверка фактически выключена.
  • Изменяемый тип list[int] в параметре там, где функция только читает: лучше Sequence[int] или Iterable[int].
  • list[Animal] не совместим с list[Dog]: список инвариантен, а Sequence ковариантен.

Что спросят на собесе

  • Проверяет ли Python типы во время выполнения, кто их проверяет.
  • Чем Optional отличается от необязательного аргумента, что такое Any и object.
  • Как написать дженерик-функцию и класс, что такое TypeVar.
  • Что такое Protocol и чем он отличается от ABC.
  • Зачем TypedDict, Literal, if TYPE_CHECKING.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.