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

В больших проектах бывает трудно с первого взгляда понять, какие данные принимает функция. Сравните два варианта одной и той же заготовки:

Python 3.13
# Что такое user? Объект? Строка с именем? Что вернёт функция?
def get_discount(user):
    pass

# Сразу понятно: передаём ID (число), получаем скидку (число с точкой)
def get_discount(user_id: int) -> float:
    pass

Приписки : int и -> float — это аннотации типов: подсказки о том, значения каких типов функция принимает и возвращает.

Подсказки, а не запреты

Python остаётся языком с динамической типизацией: во время работы программы аннотация ничего не проверяет. Убедимся:

Python 3.13
def double(x: int) -> int:
    return x * 2

print(double(5))
10
print(double("ха"))   # аннотация не остановила строку
хаха

Программа не упала: для Python аннотация — просто пометка. Пользу из неё извлекают инструменты вокруг кода — редактор и IDE. Чем именно она полезна, разберём в конце урока.

Базовый синтаксис

Для переменной тип пишут через двоеточие после имени:

Python 3.13
name: str = "Алексей"
age: int = 28
height: float = 1.82
is_developer: bool = True

В функциях аннотируют аргументы и — через стрелку -> — возвращаемое значение:

Python 3.13
def greet(name: str, age: int) -> str:
    return f"Привет, {name}! Тебе {age} лет."

message = greet("Иван", 25)
print(message)
Привет, Иван! Тебе 25 лет.

Если функция ничего не возвращает, пишут -> None.

Типизация коллекций

Для коллекций указывают и тип содержимого:

Python 3.13
numbers: list[int] = [1, 2, 3, 4, 5]

# Ключи — строки, значения — числа
user_ages: dict[str, int] = {
    "Иван": 25,
    "Анна": 22
}

# Кортеж с чёткой структурой: (строка, целое, число с точкой)
user_info: tuple[str, int, float] = ("Алексей", 30, 75.5)

Значение или None

Частый случай: функция возвращает значение — или None, если ничего не нашлось. Это записывают через вертикальную черту:

Python 3.13
def get_user_email(user_id: int) -> str | None:
    if user_id == 1:
        return "admin@example.com"
    return None

Та же черта объединяет любые типы: int | float — «целое или с точкой»:

Python 3.13
def process_price(price: int | float) -> float:
    return float(price) * 1.2

Псевдонимы типов

Чтобы не повторять длинные типы по много раз, им дают имена:

Python 3.13
Coordinates = tuple[float, float]
UserDict = dict[str, str | int]

def get_location() -> Coordinates:
    return (55.7558, 37.6173)

def process_user(user: UserDict) -> None:
    pass

Зачем всё это нужно

  1. Меньше ошибок: редактор подчеркнёт несоответствие ещё до запуска, если передать строку туда, где ожидается int.
  2. Точное автодополнение: IDE знает тип значения — значит, знает и его методы.
  3. Код проще читать: по заголовку def get_user(user_id: int) -> dict[str, str]: всё ясно без чтения тела функции.

Проверка понимания

Как описать функцию, которая принимает целое число и строку-или-None, а возвращает список чисел?