Аннотации типов
Теория
Аннотация типа — подсказка, какие типы функция ждёт и что возвращает: 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. Аннотируем параметры и результат
Тип параметра — после двоеточия, тип результата — после стрелки:
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. Описываем коллекции
Для коллекций указывают тип элементов в квадратных скобках:
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 честно об этом предупреждает:
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 не проверяет типы
Передадим количество строкой, как оно приходит из формы:
result = calc_price(1499, "2") print(type(result), len(result))
Результат:
<class 'str'> 1499
Ни ошибки, ни предупреждения: 1499 * "2" — строка из 1499 двоек. Аннотация qty: int этого не остановила.
Шаг 5. Проверяем код mypy
Такие ошибки ловит статический анализатор — он читает код, не запуская его. Файл cart.py с той же ошибкой:
def calc_price(price: int, qty: int = 1) -> int: return price * qty result = calc_price(1499, "2")
Запускаем mypy в терминале проекта:
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.