open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 04 · 01

Тайп-хинты как система: объединения, TypedDict, Literal и типизация границ в первую очередь

Аннотации в Python стираются в рантайме — их проверяет только чекер. Объединения X | Y, дженерики коллекций, TypedDict, Literal и Annotated превращают словари и строковые флаги в контракты. В легаси типизируйте сначала границы: проверенный хинт — документация, которая не врёт.

PY Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Сигнатура гласила def load_user(user_id: str) -> User: — и гласила так три года. Новый инженер, только что с TypeScript-кодовой базы, прочитал хинт, поверил ему и написал в биллинговой джобе цепочку load_user(uid).subscription.plan. В 02:40 сработал пейджер: AttributeError: 'NoneType' object has no attribute 'subscription', четыре тысячи инвойсов зависли. Функция всегда возвращала None для мягко удалённых аккаунтов — каждый ветеран команды «просто знал» это, — но аннотация ни разу в этом не призналась, потому что ничто не заставляло её признаться. Команда внедрила тайп-хинты двумя годами раньше «как документацию». Чекер так и не подключили, и документация дрейфовала ровно так же, как дрейфуют комментарии: молча, по одному рефакторингу за раз. Фикс из постмортема занял одиннадцать символов — -> User | None — плюс одна CI-джоба, которая отклонила бы это враньё в день его появления. Хинты, которые никто не проверяет, — это комментарии с синтаксисом получше.

Что рантайм на самом деле делает с хинтом: ничего

Именно это различие и объясняет, почему баг из Hook три года оставался незамеченным. Как только вы поймёте, что рантайм с аннотацией делает — а чего не делает — стратегия «типизируйте сначала границы» перестанет быть советом и станет очевидностью.

CPython вычисляет аннотации и складывает их в словарь __annotations__ (встроенный словарь аннотаций функции) — на этом всё. def f(x: int) -> str спокойно примет список и вернёт None; никакой проверки, никакого приведения, никакого предупреждения и практически нулевая цена на вызов. Это сознательное решение (PEP 484 не зря называет типы «хинтами»): язык остаётся динамическим, а контроль делегирован внешним инструментам — mypy, pyright, вашей IDE. Следствие режет в обе стороны. Ничего не ломается, если хинты неверны, — поэтому непроверяемые хинты гниют во враньё вроде -> User из Хука. Но поскольку аннотации — это интроспектируемые данные, рантайм-фреймворки читают их намеренно: pydantic строит из них валидаторы, FastAPI выводит парсинг запросов и OpenAPI-схемы, dataclasses — поля. Один синтаксис, два потребителя — статические чекеры, которые хинтам доверяют, и рантайм-библиотеки, которые их исполняют, — и путать их между собой — классическая ловушка новичка: запись x: int валидирует x не больше, чем комментарий.

Словарь системы: объединения, дженерики, TypedDict, Literal, Annotated

Прежде чем смотреть на синтаксис: прикиньте, сколько анонимных словарей гуляет по вашему сервису без имён и без проверенных форм. Каждый — контракт, живущий только в чьей-то голове. Всё, что описано ниже, существует ровно затем, чтобы эти контракты записать и отдать на проверку чекеру.

Современный синтаксис хинтов (Python 3.10+) достаточно компактен, чтобы использовать его повсюду. X | Y (PEP 604) заменяет Optional[X] и Union[X, Y]; встроенные коллекции параметризуются напрямую — list[int], dict[str, Decimal] — без импортов typing.List. Рабочие лошадки реальных кодовых баз — типы формы:

from typing import Annotated, Literal, TypedDict

class Charge(TypedDict):
    amount_cents: int
    currency: Literal["usd", "eur"]   # only these two strings type-check
    customer_id: str

class ChargeWithMeta(Charge, total=False):
    idempotency_key: str              # optional key: may be absent

UserId = Annotated[str, "uuid4 from users.id"]  # extra metadata, same type

def settle(charge: Charge, retries: list[int] | None = None) -> bool:
    if charge["currency"] == "eur":   # checker knows the literal set
        ...
    return True

TypedDict — самый недооценённый инструмент в легаси: он даёт имя и проверяемую форму анонимным словарям, которые текут через любой Python-сервис, не меняя ни байта в рантайме — Charge остаётся обычным dict, так что ни один вызов не ломается. Literal превращает строковые флаги-режимы в замкнутые множества: settle(..., currency="usdd") становится падением сборки, а не KeyError в чужом леджере в три часа ночи. Annotated прикрепляет метаданные (единицы измерения, правила валидации), которые потребляют pydantic и FastAPI, тогда как чекеры видят базовый тип. Честная оговорка: TypedDict проверяет ключи и типы значений, но по умолчанию не лишние ключи, и не спасёт от словаря, собранного руками с опечаткой в непроверяемой точке вызова — покрытие ровно настолько хорошо, насколько большую часть графа вызовов видит чекер.

Викторина

Функция аннотирована def f(x: int) -> str. В рантайме вызывающий код передаёт список. Что происходит в момент самого вызова?

Постепенная типизация легаси: сначала границы

Нетипизированный сервис на 100 тысяч строк нельзя типизировать одним героическим спринтом — попытка породит PR «перелопатить всё», который никто не отревьюит. Работающая стратегия — постепенная типизация от границ внутрь. Сначала типизируйте края: HTTP-хендлеры, функции доступа к БД, консьюмеры очередей, публичные функции каждого пакета — места, где данные входят и выходят, потому что именно там неверная форма наносит максимальный урон и именно там TypedDict или dataclass документируют контракт, от которого зависят другие команды. Внутренние хелперы поначалу могут оставаться без аннотаций: чекер считает неаннотированные функции Any и пропускает их, так что типизированная поверхность растёт без ложных тревог. Дальше — храповик: фиксируйте строгость помодульно в конфиге (секции per-module у mypy, execution environments у pyright), чтобы уже типизированные модули не могли регрессировать, и требуйте аннотации во всём новом коде. Команды, идущие этим путём, видят отдачу быстро — чекер начинает ловить настоящие баги (забытый None, перепутанный порядок аргументов, опечатки в ключах словаря) уже на первых пограничных модулях, задолго до внушительного покрытия. Антипаттерн — посыпать хинтами внутренности, пока I/O-край всё ещё гоняет сырые словари: получите шум аннотаций без ценности контракта.

Викторина

У вас неделя на внедрение типизации в нетипизированный Django-сервис на 100 тысяч строк. Где хинты дают больше всего безопасности на час работы?

Вспомните перед уходом
  1. 01
    Что рантайм Python делает с тайп-хинтами и какие два вида потребителей придают хинтам ценность?
  2. 02
    Вам достался большой нетипизированный код. Опишите стратегию постепенной типизации и почему TypedDict в ней центральный инструмент.
Итог

Типизация в Python — это контрактный слой, который рантайм сознательно игнорирует: аннотации вычисляются в __annotations__ и никогда не проверяются, так что def f(x: int) -> str без звука примет список. Контроль принадлежит потребителям — статическим чекерам вроде mypy и pyright, проверяющим код до запуска, и рантайм-библиотекам вроде pydantic и FastAPI, которые интроспектируют хинты ради валидаторов и схем. Современный словарь делает контракты дешёвыми: объединения X | Y из PEP 604, параметризованные встроенные типы вроде dict[str, Decimal], TypedDict, дающий анонимным словарям именованную проверяемую форму при том, что в рантайме это обычные dict, Literal, замыкающий множества строковых флагов, и Annotated, несущий метаданные для рантайм-инструментов. Режим отказа — непроверяемый хинт: документация, дрейфующая молча, как -> User, три года возвращавший None, пока доверчивый читатель не отправил AttributeError в прод. Стратегия для легаси — постепенная типизация от границ внутрь: сначала хендлеры, доступ к БД, консьюмеры и публичные API, потому что именно там болят ошибки формы; чекер считает неаннотированное Any, а помодульный храповик строгости не даёт типизированным модулям регрессировать. Хинты окупаются, только когда их кто-то проверяет; подключённые к чекеру, они — единственная документация, которая валит сборку, а не устаревает. Теперь, когда в PR встретите неаннотированную граничную функцию, вы знаете, что делать: поставить настоящую форму, подключить чекер к CI и сделать невозможным повторное вранье.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.