Аннотации типов в Python: практическое руководство с mypy
Практика3 минРедакция Innovialabs
Коротко
Аннотации типов не меняют поведение Python, но позволяют mypy находить ошибки до запуска; начинайте с новых и критичных модулей и ужесточайте проверки постепенно.
Что дают аннотации типов
Аннотации типов — это подсказки о том, какие данные принимает и возвращает код. Python их не проверяет во время выполнения, но их читают статические анализаторы вроде mypy и редакторы кода.
Практическая польза:
- ошибки вида «передали
Noneтуда, где ждут строку» находятся до запуска; - автодополнение и переход к определению работают точнее;
- сигнатура функции сама служит документацией;
- рефакторинг безопаснее: анализатор покажет все места, которые сломались.
Базовые аннотации
def greet(name: str, times: int = 1) -> str:
return ", ".join([f"Hello, {name}"] * times)
scores: dict[str, int] = {"alice": 10}
tags: list[str] = []
Встроенные коллекции list, dict, set, tuple можно параметризовать напрямую в современных версиях Python, импорт из typing для них не нужен.
Optional и Union
Если значение может отсутствовать, это нужно указать явно:
def find_user(user_id: int) -> User | None:
...
def parse(value: str | bytes) -> str:
return value.decode() if isinstance(value, bytes) else value
User | None — то же самое, что Optional[User], а str | bytes — то же, что Union[str, bytes]. После такой аннотации mypy заставит проверить результат на None перед использованием — это и есть главная ценность.
Generic-функции
Когда тип результата зависит от типа аргумента, используйте TypeVar:
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else None
first([1, 2]) вернёт int | None, а first(["a"]) — str | None.
TypedDict: словари с известной структурой
JSON из API часто живёт в виде словарей. TypedDict описывает ключи и их типы:
from typing import TypedDict
class UserDTO(TypedDict):
id: int
name: str
email: str | None
Опечатка в ключе или неверный тип значения станут ошибкой проверки.
Protocol: утиная типизация с проверкой
Protocol описывает, что объект умеет, без наследования:
from typing import Protocol
class Notifier(Protocol):
def send(self, text: str) -> None: ...
def alert(notifier: Notifier) -> None:
notifier.send("Server is down")
Любой класс с методом send(text: str) подойдёт — удобно для подмены зависимостей в тестах.
Как подключить mypy к существующему проекту
Включать строгий режим сразу на всём коде бессмысленно: получите сотни ошибок и желание всё отключить. Двигайтесь поэтапно.
- Установите mypy как dev-зависимость и запустите на проекте. По умолчанию он проверяет только аннотированные функции, поэтому старт мягкий.
- Добавьте базовую конфигурацию в
pyproject.toml. - Ужесточайте проверки по модулям: новые и критичные пакеты — строго, старые — позже.
- Добавьте mypy в CI, чтобы новые ошибки не попадали в основную ветку.
[tool.mypy]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "app.billing.*"
disallow_untyped_defs = true
Для сторонних библиотек без типов ищите пакеты-заглушки (stubs), а ignore_missing_imports используйте как временную меру.
Частые ошибки
- Повсеместный
Any. Он отключает проверку, и аннотация теряет смысл. # type: ignoreбез комментария. Через полгода никто не вспомнит, что именно скрывалось.- Ожидание проверки в рантайме. Для валидации входящих данных нужны отдельные инструменты, аннотации их не заменяют.
- Попытка типизировать всё за один раз. Постепенный подход надёжнее.