Type hints и mypy: сетка безопасности без изменения семантики языка
core аннотировать функцию типами и настроить mypy так, чтобы он ловил ошибки типов до запуска
Утиная типизация гибка, но не бесплатна. Если функция ожидает итерируемое со строками, а ей передали [1, 2, 3] — протокол итерации соблюдён, и ошибка вылезет глубоко внутри, на первом .upper(). Здесь вступают аннотации типов и статический анализ.
Аннотации типов (type hints, PEP 484) выглядят похоже на сигнатуры C:
def greeting(name: str) -> str:
return 'Hello ' + name
Но по семантике это совсем другая вещь: аннотации — это метаданные, которые CPython во время выполнения в общем случае не проверяет и не использует для диспетчеризации. greeting(123) спокойно дойдёт до тела функции и упадёт (или не упадёт) уже по факту исполнения операции.
Отсюда первая экспертная ошибка C-программиста: увидев type hints, он расслабляется, считая, что «теперь Python типизирован, как C». Это неверно ровно так же, как комментарий не гарантирует, что код ему соответствует. Аннотация — декларация намерения для человека и для внешнего инструмента.
Этим инструментом обычно является mypy (аналоги — pyright, pyre). Он читает те же аннотации и статически, до запуска, проверяет согласованность типов по всей кодовой базе — ту работу, которую в C бесплатно делал компилятор, но как отдельный, осознанно запускаемый шаг. Не запускаете mypy в CI или перед коммитом — аннотации остаются документацией, не защищающей ни от чего в runtime.
graph TD
A["1. Пишем код с аннотациями"] --> B{"2. Запускаем mypy"}
B -->|"ошибки найдены"| C["3. mypy: Argument 1 has incompatible type int; expected str"]
C --> D["4. Исправляем код"]
D --> A
B -->|"ошибок нет"| E["5. Запускаем python my_script.py"]Не теряется ли при этом гибкость протоколов? Нет: модуль typing даёт абстрактные типы, описывающие поведение, а не конкретные классы:
Iterable[T]— всё, что можно итерировать, отдавая элементы типаT;Sequence[T]— есть длина и доступ по индексу (__len__,__getitem__);Mapping[K, V]— поведение словаря;Protocol— формальное описание «утиного типа»: любой класс с нужными методами подходит структурно, без наследования.
from typing import Iterable
def process_strings(items: Iterable[str]) -> None:
for item in items:
print(item.upper()) # mypy знает, что item — str
process_strings(["a", "b"]) # OK
process_strings(("c", "d")) # OK
# process_strings([1, 2, 3])
# error: List item 0 has incompatible type "int"; expected "str"
Практическая связка для C-привычки к строгой типизации:
- Аннотируйте публичные функции и границы модулей — там контракт ценнее всего.
- Запускайте
mypyотдельным шагом, аналогичноgcc -Wall -Werror: до тестов, в CI, локально перед коммитом. - Не путайте прохождение
mypyс отсутствием runtime-ошибок:mypyне выполняет код и не видит динамику (getattr,setattr,eval, монки-патчинг, данные изjson.loadsбез аннотаций).
Разобранный пример
Функция с типовой C-логикой «валидации диапазона»:
def clamp(value: int, low: int, high: int) -> int:
if value < low:
return low
if value > high:
return high
return value
Без mypy это ничем не отличается от урока 1: clamp("abc", 0, 10) попытается сравнить строку с числом и упадёт с TypeError — но только в момент вызова.
Если в файле есть вызов
clamp("abc", 0, 10)
то mypy сообщит об ошибке до запуска программы:
error: Argument 1 to "clamp" has incompatible type "str"; expected "int"
Это то поведение, к которому вы привыкли в C: ошибка найдена статическим анализом всей кодовой базы, а не тем, что строка случайно исполнилась в тесте. Разница лишь в том, что шаг отдельный и необязательный: забыли запустить mypy — вернулись к чистому runtime-поведению из урока 1.
Важный нюанс: mypy проверяет согласованность объявленных типов, а не реальное поведение во время исполнения. Если внутри функции происходит setattr(obj, "x", 1) или объект пришёл из внешнего JSON без аннотации, несоответствие может остаться незамеченным — статический анализ ограничен тем, что выражено в типах явно.
Попробуй сейчас
Сохраните clamp выше в файл clamp.py, добавьте туда вызов clamp("x", 1, 2), установите mypy (pip install mypy) и запустите mypy clamp.py. Убедитесь, что ошибка типа найдена до запуска программы.
Затем измените сигнатуру на value: float и проверьте вызов clamp(1, 0, 10) — убедитесь, что mypy его пропускает (по правилам PEP 484 int принимается там, где ожидается float).
Дополнительно: замените value: int на values: Iterable[int] в новой функции и посмотрите, как mypy реагирует на передачу list[str].
Получилось, если…
Вы поняли материал, если можете объяснить коллеге одним предложением: «аннотации типов ничего не проверяют во время выполнения — проверяет их mypy, отдельным шагом, до запуска, как компилятор C делал это для сигнатур функций», и понимаете, когда писать конкретный тип (list[int]), а когда абстрактный протокол (Iterable[int]).
Вывод
Type hints — метаданные, а не runtime-механизм принуждения. Проверку, аналогичную компилятору C, делает mypy как отдельный статический шаг. Абстрактные типы из typing (Iterable, Sequence, Mapping, Protocol) сохраняют гибкость утиной типизации внутри статической проверки. Забыли запустить mypy — вернулись к чистой динамической типизации из первого урока.
flowchart TD
A[Код с аннотациями типов] --> B{Запущен mypy?}
B -->|да| C[Статическая проверка согласованности типов до запуска]
C --> D[Ошибка найдена до исполнения, как в C]
B -->|нет| E[Аннотации игнорируются интерпретатором]
E --> F[Проверка типов только в runtime, как в уроке 1]AI-generated · source-grounded review
🛡 Fact-checked: 2 risky claims verified · 4 removed · confidence: high · figures: 2
A second model re-checked this lesson's claims and corroborated 3 of 4 , and could not confirm 1 either way. This is incomplete corroboration, not a disagreement.
On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course