Аннотации типов

Теория

Аннотация типа — подсказка, какие типы функция ждёт и что возвращает: def calc_price(price: int, qty: int) -> int. Читателю сразу видно, что передать и что получить; IDE подсказывает методы и подчёркивает ошибки; инструменты вроде mypy проверяют весь проект до запуска. В современном backend аннотации — норма: на них построены FastAPI, Pydantic, сериализаторы, и без них в большом коде не разобраться.

Главное, что нужно помнить: Python сам аннотации не проверяет и ничего не преобразует. Это документация для людей и инструментов. Если в функцию с qty: int передать строку "2", она спокойно выполнится — с неправильным результатом. Поэтому данные снаружи (формы, URL, JSON) по-прежнему приводят к нужному типу явно, а аннотация лишь фиксирует договорённость.

Наглядно

Таблица 1. Как записываются типы на примерах магазина (Python 3.10+).

Запись Что значит Пример
int, str, bool, Decimal простой тип qty: int
list[int] список целых prices: list[int]
dict[int, str] словарь: ключи int, значения str names: dict[int, str]
tuple[int, int] кортеж из двух целых -> tuple[int, int]
dict | None словарь или None -> dict | None — «может не найтись»
x: int | None = None необязательный параметр discount: int | None = None
-> None функция ничего не возвращает def log(msg: str) -> None

Таблица 2. Что аннотации делают и чего не делают.

Делают Не делают
документируют договорённость о типах не проверяют типы при запуске
помогают IDE подсказывать и находить ошибки не преобразуют "2" в 2
позволяют mypy проверить проект до запуска не замедляют и не ускоряют код

На практике

Задача: описать типами функции корзины и каталога и разобраться, где аннотации помогают, а где — нет.

Шаг 1. Аннотируем параметры и результат

Тип параметра — после двоеточия, тип результата — после стрелки:

python
def calc_price(price: int, qty: int = 1) -> int:
    return price * qty

print(calc_price(1499, 2))
print(calc_price.__annotations__)

Результат:

вывод
2998
{'price': <class 'int'>, 'qty': <class 'int'>, 'return': <class 'int'>}

Аннотации хранятся в функции, но на выполнение не влияют.

Шаг 2. Описываем коллекции

Для коллекций указывают тип элементов в квадратных скобках:

python
def calc_total(prices: list[int]) -> int:
    return sum(prices)

def price_range(prices: list[int]) -> tuple[int, int]:
    return min(prices), max(prices)

print(calc_total([1499, 490]), price_range([1499, 490]))

Результат:

вывод
1989 (490, 1499)

Шаг 3. Отмечаем, что результата может не быть

Товара с таким id может не оказаться — тип результата dict | None честно об этом предупреждает:

python
def find_product(catalog: dict[int, dict], product_id: int) -> dict | None:
    return catalog.get(product_id)

catalog = {42: {"name": "Мышь", "price": 1499}}
print(find_product(catalog, 42))
print(find_product(catalog, 7))

Результат:

вывод
{'name': 'Мышь', 'price': 1499}
None

Тот, кто вызывает find_product, видит | None и обязан обработать случай «не найден».

Шаг 4. Убеждаемся, что Python не проверяет типы

Передадим количество строкой, как оно приходит из формы:

python
result = calc_price(1499, "2")
print(type(result), len(result))

Результат:

вывод
<class 'str'> 1499

Ни ошибки, ни предупреждения: 1499 * "2" — строка из 1499 двоек. Аннотация qty: int этого не остановила.

Шаг 5. Проверяем код mypy

Такие ошибки ловит статический анализатор — он читает код, не запуская его. Файл cart.py с той же ошибкой:

python
def calc_price(price: int, qty: int = 1) -> int:
    return price * qty

result = calc_price(1499, "2")

Запускаем mypy в терминале проекта:

bash
mypy cart.py

Результат:

вывод
cart.py:4: error: Argument 2 to "calc_price" has incompatible type "str"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

В тренажёре mypy нет, но в проектах его (или проверку типов в IDE) запускают в CI.

Частые ошибки

  • Аннотация как проверка. qty: int не превращает "2" в 2 и не выбрасывает ошибку. Данные снаружи приводят к типу явно.
  • -> dict у функции, которая может вернуть None. Вызывающий код не ждёт None и падает на result["name"]. Пишите -> dict | None и обрабатывайте None.
  • def f(x: int = None). Значение по умолчанию не подходит под тип; правильно — x: int | None = None.
  • list вместо list[int]. Формально верно, но теряется главное — что лежит внутри.
  • Изменяемый объект по умолчанию с аннотацией. items: list[int] = [] — та же ловушка, что без аннотации.

Итог

Синтаксис

  • param: тип, -> тип_результата; по умолчанию — param: тип = значение.
  • Коллекции: list[int], dict[int, str], tuple[int, int]; «или» — int | None.

Что дают

  • Документацию, подсказки IDE, проверку mypy до запуска; на них построены FastAPI и Pydantic.

Чего не дают

  • Python не проверяет и не преобразует типы при выполнении.
  • | None в результате — сигнал вызывающему коду: обработай отсутствие значения.

Вопросы на собеседовании

Проверяет ли Python аннотации типов при выполнении?

Нет. Аннотации сохраняются в __annotations__, но интерпретатор их не проверяет. Проверку делают статические анализаторы (mypy, pyright) или библиотеки, которые читают аннотации сами, — например, Pydantic и FastAPI проверяют входные данные по ним.

Как аннотировать необязательный параметр и результат, которого может не быть?

x: int | None = None и -> dict | None (до Python 3.10 — Optional[int] из typing). | None означает, что вызывающий код должен обработать None.

Зачем нужны аннотации, если Python их игнорирует?

Они делают код самодокументируемым, позволяют IDE и mypy находить ошибки типов до запуска и служат источником схемы для библиотек: FastAPI по аннотациям валидирует запросы и генерирует документацию API.

Ошибка в тексте

Сообщить об ошибке могут зарегистрированные пользователи — так в сообщениях меньше спама.

Войти, чтобы сообщить