Чекеры в бою: сужение типов, строгость, зараза Any и mypy против pyright
Чекер верифицирует то, что код доказывает: сужение через isinstance и TypeGuard/TypeIs, строгие флаги, запрет неявного Optional. Any расползается молча и отключает проверку. mypy и pyright различаются выводом типов и скоростью; reveal_type показывает, во что верит чекер.
Рубильник ничего не отрубил. Во время инцидента с чекаутом дежурный выключил флаг new_checkout и девять минут смотрел, как уровень ошибок не шевелится, пока не обошёл флаг-сервис целиком. Постмортем нашёл баг в одиннадцати строках: def is_enabled(flag: str) -> bool: заканчивалась на return FLAGS.get(flag). Для любого флага, отсутствующего в кеше — а редеплой во время инцидента кеш как раз опустошил, — она возвращала None, а не False. Все вызовы вида if is_enabled(...) работали случайно: None фальсивен. Но дашборд инцидента был написан аккуратно: if is_enabled("new_checkout") is False: disable(). А None is False не истинно никогда. mypy в strict-режиме помечает эту строку возврата дословно — Incompatible return value type (got "bool | None", expected "bool") — и хинты у команды были повсюду. Они просто ни разу не прогнали по ним чекер. Аннотация была обещанием; ничто в пайплайне не делало её доказательством.
Сужение: что чекер реально может доказать
Чекер — это прувер над вашим control flow. Видя value: str | bytes | None, он отслеживает, как каждая ветка ужимает объединение: if value is None: return устраняет None до конца функции; isinstance(value, bytes) расщепляет остаток; морж if (m := pattern.match(s)): сужает m до Match[str] внутри блока; match сужает по веткам case. Когда встроенное сужение не видит сквозь ваши хелперы, его учат: TypeGuard (PEP 647) объявляет, что булева функция доказывает тип в своей истинной ветке, а TypeIs (PEP 742, Python 3.13) — более воспитанный преемник, сужающий обе ветки:
from typing import Any, TypeGuard
def is_str_list(xs: list[Any]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in xs)
def handle(value: str | bytes | None) -> str:
if value is None:
return ""
if isinstance(value, bytes):
reveal_type(value) # note: Revealed type is "bytes"
return value.decode()
reveal_type(value) # note: Revealed type is "str"
return valuereveal_type() — отладчик всего этого: фиктивная функция, понятная только чекеру (в старых Python она падает в рантайме; с 3.11 настоящая есть в typing), печатающая, кем чекер считает значение ровно на этой строке. Когда чекер отвергает код, который вы «точно знаете, что корректен», продуктивный ход — не спорить, а вставить reveal_type и обнаружить, что в объединении всё ещё живёт член, который вы якобы устранили. Острый край TypeGuard: чекер слепо доверяет вашей реализации. Гард, возвращающий True для неверной формы, — дырка, пробитая в доказательстве, причём с полной уверенностью чекера за спиной.
Any — зараза; строгость — карантин
Прежде чем записывать «80% покрытия аннотациями» в успех, спросите себя: откуда данные реально входят в кодовую базу? Если на дне цепочки вызовов сидит сторонний клиент или нетипизированный хелпер, эти восемьдесят процентов могут не покрывать почти ничего.
Any (специальный тип, совместимый со всем в обе стороны и отключающий проверку) совместим со всем в обе стороны — и он расползается. Значение из нетипизированной библиотеки — Any; проиндексируйте его, сложите с чем-то, передайте дальше — и каждый результат тоже Any. Пути кода, тронутые Any, попросту не проверяются, выглядя при этом неотличимо от проверенных. Вот почему «у нас 80% покрытия аннотациями» может значить куда меньше, чем звучит: один нетипизированный клиент на дне цепочки вызовов тихо превращает целые модули обратно в динамический Python. Флаги строгости — карантин. --strict у mypy связывает главные: --disallow-untyped-defs (каждая функция аннотирована), --disallow-any-generics (никаких голых list), --warn-return-any, а неявный Optional современный mypy отвергает уже по умолчанию — -> bool, возвращающий None, из Хука умирает прямо там. Режим strict у pyright — эквивалент, плюс диагностики в духе reportUnknownMemberType, которые показывают Any, утекающий из библиотек, вместо молчаливого принятия. Полный strict на легаси за один день не включает никто; рабочий паттерн — помодульный храповик: strict для новых и пограничных модулей, basic для остального, и конфиг только затягивается.
Функция получает данные через нетипизированную клиентскую библиотеку, и чекер видит результат как Any. Что чекер верифицирует в коде, использующем этот результат?
mypy против pyright и превращение доказательства в CI-гейт
Два продовых чекера согласны в ядре, но полезно расходятся. mypy — референсная реализация: на Python, медленнее на больших кодовых базах (демон dmypy делает тёплые перепроверки быстрыми), с системой плагинов, понимающей магию Django и SQLAlchemy, — в этих стеках это часто решающий фактор. pyright — чекер Microsoft на TypeScript, движок внутри Pylance/VS Code: заметно быстрее на больших репозиториях (полные прогоны, занимающие у mypy минуты, обычно укладываются в секунды или десятки секунд), с watch-режимом и ощутимо более сильным выводом типов — pyright выводит типы возврата неаннотированных функций и проверяет их тела, тогда как mypy по умолчанию считает неаннотированные функции Any и пропускает их. Одно это различие означает, что pyright находит реальные баги в коде, который mypy вообще не читает, — и оно же объясняет волну новых ошибок в первый день внедрения pyright. Честный совет: любой из двух радикально лучше, чем ничего; pyright — за скорость и вывод, mypy — за плагинозависимые ORM; гонять оба разумно, потому что они ловят непересекающиеся проблемы. Какой бы вы ни выбрали, он обязан быть CI-гейтом — «чекер только на ноутбуке» и есть способ получить Хук. Стандартная схема: прогон на каждом PR с кешированием, падение сборки на любой новой ошибке и запиненная версия чекера, чтобы апгрейды (добавляющие проверки) приезжали осознанными PR, а не внезапно красной сборкой.
В файле есть def f(): return 42 без аннотаций, а вызывающий код делает f() + 'x'. Что сообщат mypy (по умолчанию) и pyright?
- 01Как работает сужение, что добавляют TypeGuard и TypeIs и как отлаживать сужение, которое не происходит?
- 02Почему Any называют заразой и что конкретно различается у mypy и pyright на неаннотированном коде?
Чекер — это прувер над вашим control flow, и доказать он может лишь то, что код делает доказуемым. Объединения сужаются ветка за веткой — гарды is None, isinstance, привязки моржа, ветки match, — а пользовательские предикаты входят в доказательство через TypeGuard (PEP 647, только истинная ветка, доверие слепое) и TypeIs (PEP 742, обе ветки). reveal_type — отладчик: печатает веру чекера на строке и обычно вскрывает член объединения, который вы не устранили. Враг доказательства — Any: совместимый со всем, распространяющийся через каждую операцию, он молча выключает проверку на целых путях кода, которые по-прежнему выглядят типизированными, — один нетипизированный клиент способен отменить аннотации целого модуля. Флаги строгости — карантин: disallow-untyped-defs, warn-return-any, запрет неявного Optional (ровно та проверка, что ловит баг рубильника из Хука — -> bool, возвращающий None, из-за которого is False не срабатывал никогда), — применяемый помодульным храповиком. mypy и pyright делят ядро, но расходятся там, где важно: pyright выводит и проверяет неаннотированные функции и работает заметно быстрее; mypy по умолчанию пропускает неаннотированные тела, зато несёт систему плагинов, на которую опираются стеки Django и SQLAlchemy. Оба остаются обещаниями, пока их не запускает CI: гейт на каждом PR, падение на новых ошибках, запиненная версия чекера, чтобы апгрейды приезжали осознанно. Теперь, когда встретите -> bool, возвращающий что-то фальсивое, но не False, вы точно знаете, какой флаг чекера это поймал бы — и что именно добавить в CI, чтобы следующий инженер не тратил девять минут, глядя на неизменный график ошибок.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.