Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Типизация в Python и mypy: внедрение без боли

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

Как внедрить статическую типизацию в Python-проект: что дают аннотации, настройка mypy, gradual-стратегия от границ к ядру, типичные типы (Optional, TypedDict, Protocol) и честные ограничения.

Прямой ответ

Типизация в Python — опциональная и постепенная: аннотации не влияют на рантайм, а проверяются статически (mypy/pyright) в CI или редакторе. Рабочая стратегия внедрения — gradual, от границ к ядру: сначала типизируются новые модули и публичные API (функции, с которыми работает команда), потом внутренние реализации, Any и # type: ignore — временные инструменты с планом выжигания, а не постоянный стиль. Выигрыш проявляется быстро: ошибки «None не имеет атрибута» и перепутанные аргументы ловятся до запуска, а рефакторинг перестаёт быть прыжком в неизвестность.

Что дают аннотации на практике

Аннотации — def f(x: int, name: str | None = None) -> list[str]: — читаются как контракт и машиной, и человеком. Три конкретных эффекта. Первый — автодополнение и навигация в редакторе становятся точными: IDE знает тип переменной после любой цепочки вызовов. Второй — класс багов умирает до запуска: передача строки туда, где ждали число; обращение к атрибуту опционального значения без проверки; возврат не того типа. Третий — документация, которая не врёт: сигнатура с типами точнее любого docstring, потому что проверяется. Современный синтаксис (Python 3.10+) — X | Y вместо Union, list[str] вместо List[str] — короче и читается легче.

Настройка mypy: строгость по нарастающей

Начинать с --strict на живом проекте — способ получить тысячу ошибок и забросить. Разумная лестница: сначала базовый mypy на новом коде (в pre-commit или CI — интеграция через git-хуки описана в отдельной статье), затем --disallow-untyped-defs для новых модулей через per-module-секции конфига, затем --warn-return-any и --strict по мере выжигания Any. Полезный режим для постепенности — --follow-imports=silent (проверять только явно типизированное, импорты не тянуть в ошибки) и # type: ignore[error-code] с конкретным кодом — глушить всю категорию «насовсем» плохая привычка: игнор без кода скроет будущие настоящие ошибки.

Словарь полезных типов

Повседневный набор, который покрывает большинство случаев: Optional/X | None — значение может отсутствовать (и mypy заставит проверить); TypedDict — словарь фиксированной структуры (ответы API, конфиги) без класса; Literal['a', 'b'] — поле из закрытого набора; Protocol — «утиная типизация»: объект подходит, если у него есть нужные методы, без наследования (идеально для интерфейсов и моков в pytest); TypeVar и дженерики — list[T], функции над «любым T»; Callable[[int, str], bool] — сигнатура коллбэка; overload — разный тип возврата от типа аргумента. Отдельная дисциплина — границы данных: на входе из внешнего мира (JSON, env) типы не гарантированы ничем, и там статическая типизация стыкуется с рантайм-валидацией — связка с pydantic разбирается в статье про выбор между pydantic и dataclasses.

Где типизация не спасает: честные границы

Статический анализ видит только то, что описано: данные из JSON/БД приходят как Any/dict, пока их не прогнали через валидатор; # type: ignore и касты (cast) могут замаскировать реальную ошибку; динамические конструкции (getattr по строке, метаклассы, декораторы без functools.wraps и без правильных сигнатур) для mypy непрозрачны; сторонние библиотеки без py.typed/type stubs требуют самописных стабов или глушения. Поэтому типизация — слой защиты, а не замена тестам: тесты проверяют поведение, типы — форму контрактов. Вместе они закрывают существенно больше, чем по отдельности, и это аргумент для команды, а не «ещё один линтер».

Gradual-план для живого проекта

Практическая последовательность, проверенная на проектах: неделя первая — добавить mypy в CI в мягком режиме (не блокирующем) и типизировать один новый модуль; далее — включить per-module строгость для пакета, который активно развивается (частые баги окупят типизацию быстрее всего); затем — публичные интерфейсы остальных модулей (сигнатуры без реализаций); параллельно — выжигать Any на границах данных (pydantic-модели вместо dict). Метрика прогресса — доля покрытых типами определений (mypy --strict по модулям) и число type: ignore (должно убывать). Обратная сторона честности: на прототипах и склеивающих скриптах типизация часто избыточна — её ценность растёт с размером кода и числом людей.

Практический сценарий: типизация модуля за час

Возьмём типичный нетипизированный модуль сервиса — pricing.py с расчётом цены: функция принимает корзину (dict) и промокод (str или None), ходит в каталог и возвращает число. Первый проход — сигнатуры: def calculate(cart: Cart, promo: str | None = None) -> Decimal, где Cart — TypedDict с полями items/total (структура словаря становится контрактом, а mypy начинает видеть ошибки доступа к несуществующим ключам). Запуск mypy сразу показывает настоящие баги: в ветке промокода результат иногда None (Optional[Decimal] утекает в арифметику ниже — до типизации это был редкий прод-инцидент «NoneType не складывается»). Второй проход — границы: ответ каталога приходит из HTTP как dict — описываем pydantic-моделью CatalogPrice и валидируем на входе (статические типы и рантайм-валидация стыкуются именно на границах). Третий — возврат: Decimal вместо float для денег (модель домена) — mypy ловит три места, где цена тихо складывалась с float. Итого час работы: две настоящие ошибки найдены до прода, автодополнение в редакторе заработало, рефакторинг модуля перестал быть прыжком веры. Остальные модули типизируются по мере касания — gradual в действии: никто не останавливал разработку ради «проекта типизации».

FAQ

Что выбрать: mypy или pyright? Для командной CI-проверки и gradual-конфигов mypy — стандарт де-факто; pyright (основа Pylance) быстрее и строже «из коробки», хорош локально в VS Code. Конфликтов нет: многие держат mypy в CI, pyright в редакторе.

Замедляют ли аннотации рантайм? Нет: аннотации не исполняются (хранятся лениво/в __annotations__), влияние на производительность пренебрежимо. Медленным может быть импорт typing в микросервисах на холодном старте — лечится from __future__ import annotations.

Обязательно ли типизировать всё? Нет — gradual и есть смысл PEP 484. Ценность концентрируется на границах модулей и данных; внутренние хелперы из пяти строк часто не стоят аннотаций.

Как типизировать легаси-код без переписывания? Начать с сигнатур (без тел) и стабов (.pyi) для самых используемых модулей, --follow-imports=silent, игноры с кодами. Полное покрытие легаси — цель на месяцы, и она не обязательна.

Источники: документация typing (docs.python.org), mypy «Using mypy with existing code», PEP 484 — ссылки в шапке.