← назад к разделу

Python — язык с динамической типизацией: переменная не привязана к типу, и одно и то же имя может сегодня хранить число, а завтра строку. Это удобно, пока программа маленькая. Но когда код растёт, отсутствие явных типов превращается в источник ошибок и затрудняет чтение. Аннотации типов (type hints) — способ записать ожидаемые типы прямо в коде, не меняя его поведения.

Зачем типы в динамическом языке

Представь функцию без аннотаций:

def discount(price, percent):
    return price - price * percent / 100

Что сюда передавать? price — число или строка с ценой? percent — доля (0.2) или проценты (20)? Из сигнатуры это не видно — приходится читать тело или гадать. А если кто-то вызовет discount("100", 10), ошибка вылезет только во время выполнения, и не сразу.

Аннотации делают контракт явным:

def discount(price: float, percent: float) -> float:
    return price - price * percent / 100

Теперь сразу понятно: на входе два числа, на выходе число. Эта запись — подсказка для человека и инструментов, а не команда интерпретатору. Python её не проверяет при запуске (об этом ниже), но она резко повышает читаемость и ловит ошибки ещё до старта программы.

Синтаксис аннотаций

Базовая форма — двоеточие после имени и тип, для возвращаемого значения — стрелка ->.

name: str = "Anna"          # переменная
age: int                    # объявление без значения тоже допустимо

def greet(user: str) -> str:
    return f"Привет, {user}"

def log(message: str) -> None:  # ничего не возвращает
    print(message)

Аннотировать можно простыми встроенными типами: int, float, str, bool, bytes. Тип None (а точнее его аннотация None) ставят, когда функция ничего не возвращает.

Коллекции: list[int], dict[str, int]

Начиная с Python 3.9 встроенные коллекции можно параметризовать прямо так — указывая тип элементов в квадратных скобках:

def total(prices: list[float]) -> float:
    return sum(prices)

scores: dict[str, int] = {"Anna": 5, "Ivan": 4}   # ключ str, значение int
coords: tuple[float, float] = (55.75, 37.61)       # пара чисел
unique_ids: set[int] = {1, 2, 3}

Это сразу говорит читателю: prices — список именно чисел, а scores — словарь «строка → целое». Раньше для этого приходилось импортировать List, Dict из модуля typing; в современном коде (3.9+) хватает встроенных типов.

Модуль typing: Optional, Union, |, Callable

Для более сложных случаев есть стандартный модуль typing.

Значение может быть None. Часто функция возвращает либо объект, либо None (например, «не нашли»). Это записывают через Optional или, что сейчас предпочтительнее, через | None:

from typing import Optional

def find_user(user_id: int) -> Optional[str]:   # str или None
    ...

def find_user(user_id: int) -> str | None:      # то же самое, современный стиль
    ...

Несколько допустимых типов. Если значение может быть одного из нескольких типов — это Union. С Python 3.10 вместо Union[A, B] пишут просто A | B:

from typing import Union

def parse(value: Union[int, str]) -> int:  # принимает int или str
    return int(value)

def parse(value: int | str) -> int:        # современный синтаксис
    return int(value)

Короткая формула: Optional[X] — это то же, что X | None.

Функция как аргумент. Когда параметр — это другая функция (callback), её тип описывают через Callable: в скобках типы аргументов, после — тип результата:

from typing import Callable

# принимает функцию (int, int) -> int и применяет её
def apply(op: Callable[[int, int], int], a: int, b: int) -> int:
    return op(a, b)

apply(lambda x, y: x + y, 2, 3)   # 5

mypy: статическая проверка типов

Сам Python аннотации не проверяет. Чтобы они приносили пользу как «защита от опечаток», нужен статический анализатор — самый популярный называется mypy. Он читает код вместе с аннотациями и сообщает о несоответствиях, не запуская программу.

def discount(price: float, percent: float) -> float:
    return price - price * percent / 100

discount("100", 10)   # строка вместо числа
pip install mypy
mypy app.py
# app.py:4: error: Argument 1 to "discount" has incompatible type "str"; expected "float"

Ошибка найдена до запуска — на этапе проверки, а не когда пользователь нажал кнопку в проде. Чем больше проект, тем заметнее выигрыш: mypy ловит целый класс ошибок, которые иначе всплыли бы во время выполнения.

Аннотации не влияют на выполнение

Важно понять: интерпретатор Python игнорирует аннотации при работе программы. Они не приводят типы, не проверяют их и не замедляют код. Это просто метаданные.

def square(n: int) -> int:
    return n * n

print(square("ab"))   # никакой ошибки типов — Python просто выполнит "ab" * "ab"... 

Вызов square("ab") нарушает аннотацию, но Python всё равно попытается выполнить тело. Здесь он упадёт уже на самой операции (str * str запрещено), но не из-за аннотации, а из-за реальной несовместимости в рантайме. Аннотации сработали бы раньше — если бы код прогнали через mypy.

Технически аннотации доступны в рантайме через атрибут __annotations__, чем пользуются библиотеки вроде Pydantic и FastAPI — но это уже их собственная логика, а не встроенная проверка языка:

print(square.__annotations__)   # {'n': <class 'int'>, 'return': <class 'int'>}

Польза для IDE и поддержки

Главная ежедневная выгода от аннотаций — это инструменты и читаемость:

  • IDE подсказывает. Зная тип переменной, редактор предлагает её методы (.upper() для строки, .append() для списка) и подсвечивает явно неверные обращения.
  • Документация в сигнатуре. Типы в объявлении функции часто заменяют половину комментария — контракт виден сразу, без чтения тела.
  • Безопасный рефакторинг. Изменил тип в одном месте — статический анализатор покажет все места, где код перестал ему соответствовать.
  • Командная работа. Новому человеку проще понять чужой код, когда типы проговорены явно.

Аннотации не обязательны — Python работает и без них. Но в боевых проектах их добавляют именно ради этого: код становится самодокументируемым, а целый класс ошибок ловится автоматически.

Коротко

  • Python динамически типизирован, но аннотации типов позволяют явно записать ожидаемые типы прямо в коде.
  • Синтаксис: name: тип, для функций — def f(x: int) -> str.
  • Коллекции параметризуются: list[float], dict[str, int], tuple[float, float].
  • Модуль typing и операторы дают Optional[X] / X | None, Union[A, B] / A | B, Callable[[...], R].
  • Аннотации не влияют на выполнение — интерпретатор их игнорирует; это метаданные для людей и инструментов.
  • mypy проверяет типы статически, до запуска, и ловит несоответствия как ошибки.
  • Реальная польза — подсказки IDE, читаемость, безопасный рефакторинг и самодокументируемый код.

Что почитать дальше

  • Функции и модули — где аннотации применяются чаще всего.
  • Объектно-ориентированное программирование — типы для классов и атрибутов.
  • Инструменты разработки — как подключить mypy в рабочий процесс.