Проект растёт восемь лет, в коде триста тысяч строк, и очередное изменение типа поля в общей модели приносит ревью на сорок файлов, из которых автор тридцать не открывал. Инженер в этом месте обычно думает о статической типизации: включить mypy, получить проверку аннотаций до запуска и перестать находить ошибку несоответствия типов в продакшене. Вторая половина правды приходит позже: аннотации это код, их придётся писать, читать, обновлять и чинить, когда проверка жалуется на легаси, написанное до всяких PEP. Ниже разбор обеих сторон: что mypy реально ловит в живом коде, сколько стоит проверка на большой кодовой базе, как аннотации усложняют эволюцию API и где проходит точка безубыточности.
Постепенная типизация и уровни строгости от легаси до флага strict
Идея типов в Python строилась как необязательная надстройка. PEP 484, появившийся в 2014 году, ввёл модуль typing, и с Python 3.5 аннотации стали частью синтаксиса, не меняя исполнения: интерпретатор хранит их в __annotations__ и не проверяет. Проверкой занимается внешний инструмент, и mypy, начатый Юккой Лехтосало, стал де-факто стандартом этой роли. К январю 2023 года инструмент дошёл до версии 1.0, после чего развитие идёт размеренными минорными релизами, и это само по себе аргумент для консервативных команд. Ключевое слово подхода это постепенность: один проект живёт с полностью нетипизированными модулями и строгим new-кодом одновременно, и это штатный режим, а не компромисс.
Управление строгостью вынесено в конфигурацию, типично в pyproject.toml:
[tool.mypy]
python_version = "3.12"
strict = true
[[tool.mypy.overrides]]
module = "legacy.*"
ignore_errors = true
Флаг strict это пакет из десятка проверок: запрет неаннотированных определений, запрет неявных Any, требование аннотаций для всех аргументов и результатов. Освобождение легаси через overrides позволяет двигаться в строгий мир частями, включая его для новых модулей по мере готовности. Популярный порядок такой: сначала типизируют границы модулей, публичные сигнатуры, затем тела, и лишь в конце включают строгие проверки внутри. Проверить жёсткость на отдельном файле можно и без переезда конфига целиком:
mypy src/
mypy --strict src/new_module.py
Первая команда гоняет проект по действующим правилам, вторая просеивает один новый модуль через строгий фильтр ещё до того, как правило станет общим. Такой двойной режим снимает страх внедрения: строгий мир не наступает сразу, он протаптывается по одному файлу, и каждая строка отчёта остаётся объяснимой.
Попытка типизировать всё сразу на восьмилетнем проекте заканчивается одинаково: три тысячи ошибок, отмена задачи, недоверие к инструменту.
Реальные классы ошибок, которые mypy находит до запуска
Основная добыча mypy это не ошибки дизайна, а будничные несоответствия, которые раньше всплывали бы в рантайме на конкретной ветке. Типичные представители: обращение к атрибуту, который может отсутствовать, передача str туда, где ждут int, потерянная ветка с None после dict.get, перепутанный порядок аргументов одного типа. Проверка срабатывает на этапе сборки, до тестов и до выката:
def load(order_id: int | None) -> Order:
if order_id is None:
return default_order()
return repo.get(order_id) # mypy видит здесь уже int
Для ручной проверки выражений существует reveal_type, работающий и без аннотаций вокруг:
reveal_type(repo.get(order_id)) # note: Revealed type is "Order"
Отдельная ценность это каскадные изменения. Поле amount сменило тип с int на Decimal, и mypy перечисляет все точки, где это отразится, файл за файлом, включая места, о которых автор изменения не подозревал. Без проверки такой тур по кодовой базе занял бы дни чтения или годы ожидания багов.
Оговорка о пределах проверена. Текст ошибок сам говорит за себя, у каждой указан файл, строка и код вида:
error: Item "None" of "Optional[Order]" has no attribute "amount" [union-attr]
Оговорка о пределах: mypy не исполняет код. Динамические словари из JSON, значения с макетом Any, сборка атрибутов в цикле остаются за бортом его понимания. Проверка ловит несоответствия там, где есть честные аннотации, и молчит там, где их нет, что честно и одновременно коварно.
Скорость проверки на триста тысяч строк и постоянный процесс dmypy
Обратная сторона пользы это время прогона. mypy это интерпретируемая программа, полная проверка большой кодовой базы холодным запуском идёт минутами, и на CI такое съедает бюджет конвейера ежедневно. Стандартное решение это режим dmypy: постоянный процесс, держащий состояние проверки в памяти. По документации инструмента повторные прогоны через постоянный процесс могут быть более чем в десять раз быстрее командной строки, особенно при правке нескольких файлов, что и составляет типичный цикл разработки. Команды управления просты:
dmypy run -- src/
dmypy stop
Первый запуск такого процесса обрабатывает весь код и занимает то самое время полного прогона, последующие пересматривают только изменённое и зависимое. Ещё один ускоритель это mypyc, компилятор, собирающий сам mypy в машинный код, и скомпилированная версия работает в разы быстрее интерпретируемой. Для команд, которым нужна реакция мгновением, существует и pyright, проверка от Microsoft на TypeScript, быстрый и параллельный, но с собственным поведением на отдельных конструкциях, так что проекты нередко держат mypy в CI и pyright в редакторе.
Кэш это половина успеха на CI. Каталог .mypy_cache при повторных прогонах превращает минуты в секунды, а удалённое кэширование позволяет не греть его заново на каждом воркере. Зрелая конфигурация без кэша на большой монорепе это штраф, который платит каждый пайплайн: