Перейти к содержанию
Innovialabs

Аннотации типов в Python: практическое руководство с mypy

Практика3 мин

Коротко

Аннотации типов не меняют поведение 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 к существующему проекту

Включать строгий режим сразу на всём коде бессмысленно: получите сотни ошибок и желание всё отключить. Двигайтесь поэтапно.

  1. Установите mypy как dev-зависимость и запустите на проекте. По умолчанию он проверяет только аннотированные функции, поэтому старт мягкий.
  2. Добавьте базовую конфигурацию в pyproject.toml.
  3. Ужесточайте проверки по модулям: новые и критичные пакеты — строго, старые — позже.
  4. Добавьте 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 без комментария. Через полгода никто не вспомнит, что именно скрывалось.
  • Ожидание проверки в рантайме. Для валидации входящих данных нужны отдельные инструменты, аннотации их не заменяют.
  • Попытка типизировать всё за один раз. Постепенный подход надёжнее.

Частые вопросы

Нет, заметного влияния на скорость выполнения они не оказывают: интерпретатор их не проверяет.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.