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

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

Сравните два варианта одной и той же функции:

Python 3.13
# Было: что такое user? Объект? Данные из базы? Что вернет функция?
def get_discount(user):
    pass

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

Аннотации помогают разработчикам и IDE (среде разработки) точно понимать, какие типы данных ожидаются в разных частях программы, что значительно ускоряет написание и отладку кода.

Что такое аннотации типов?

Аннотации типов — это специальный синтаксис, который позволяет явно указывать ожидаемые типы данных для переменных, аргументов функций и возвращаемых значений.

Важно понимать: Python остаётся языком с динамической типизацией. Аннотации типов — это лишь подсказки. Сам Python не остановит выполнение программы, если вы передадите строку вместо числа. Однако они незаменимы для:

  • Улучшения автодополнения кода в IDE (PyCharm, VS Code)
  • Статического анализа кода (например, линтером mypy)
  • Самодокументируемости кода (код проще читать и понимать)

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

Аннотация переменных

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

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 лет.

В этом примере:

  • name: str — ожидается строка
  • age: int — ожидается целое число
  • -> str — функция обязуется вернуть строку

(Примечание: если функция ничего не возвращает, мы используем тип возвращаемого значения None, например -> 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)

# Множество строк
>>> unique_names: set[str] = {"Иван", "Анна", "Петр"}

Модуль typing

Для более сложных сценариев используется встроенный модуль typing:

Optional (Опциональное значение)

Если переменная может содержать значение определенного типа или None, используйте Optional:

Python 3.13
>>> from typing import Optional

>>> def get_user_email(user_id: int) -> Optional[str]:
...     if user_id == 1:
...         return "admin@example.com"
...     return None  # Возвращаем None, если пользователь не найден

Union (Объединение типов)

Когда переменная может принимать один из нескольких возможных типов:

Python 3.13
>>> from typing import Union

# Функция может принимать как целое число, так и число с точкой
>>> def process_price(price: Union[int, float]) -> float:
...     return float(price) * 1.2  # Добавляем 20% НДС

Callable (Функции как аргументы)

Если вы передаете функцию как аргумент в другую функцию, её тоже можно типизировать.

Callable принимает два аргумента: список типов входных параметров и тип возвращаемого значения:

Python 3.13
>>> from typing import Callable

>>> def apply_twice(value: int, func: Callable[[int], int]) -> int:
...     return func(func(value))

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

>>> result = apply_twice(3, double)  # double(double(3)) = 12

Создание собственных типов

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

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]:, вы мгновенно понимаете, что функция принимает числовой ID и возвращает словарь. Не нужно вчитываться в тело функции.

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

Давайте проверим ваши знания об аннотациях типов:

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