Аннотации типов Python: коллекции, объединения и None

Аннотации описывают ожидаемые типы аргументов, результатов и переменных. Они помогают редактору и статическому анализатору находить несогласованные операции до запуска, но…

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

Коллекции и вложенные типы

В современном Python запись list[str] означает список строк, dict[str, int] — словарь со строковыми ключами и целыми значениями, а tuple[int, int] — пару целых чисел. Вложенность отражает реальную форму: dict[str, list[int]] читается как группы чисел по строковому имени.

Начиная с Python 3.10 объединение записывают оператором |: str | None означает, что значение является строкой или отсутствует. До обращения к методам строки нужно сузить тип проверкой is None. Для поддержки более старых версий синтаксис и импорты выбирают в соответствии с целевой версией проекта.

Рабочий пример: безопасный поиск

def find_email(
    users: list[dict[str, str]],
    user_id: str,
) -> str | None:
    for user in users:
        if user['id'] == user_id:
            return user['email']
    return None


def email_label(email: str | None) -> str:
    if email is None:
        return 'адрес не найден'
    return email.lower()


data = [{'id': '42', 'email': 'Student@Example.com'}]
print(email_label(find_email(data, '42')))
print(email_label(find_email(data, '7')))

Ожидаемый результат:

student@example.com
адрес не найден

Возвращаемый тип заставляет вызывающий код учитывать обе ветви. Проверка is None точнее проверки истинности: пустая строка может быть допустимым строковым значением, но if not email смешает её с отсутствием.

None не исчезает от аннотации

Если сразу написать find_email(data, '7').lower(), статический анализатор предупредит, что у None нет lower, а при запуске соответствующей ветви возникнет AttributeError. Сначала сохраните результат, явно обработайте None и только затем используйте строковый метод. Не заглушайте предупреждение необоснованным cast: он сообщает анализатору предположение, но не преобразует и не проверяет объект во время выполнения.

Другая ошибка — считать, что items: list[int] = ['1'] автоматически превратит строку в число. Аннотация ничего не конвертирует. Запустите анализатор типов в конфигурации проекта и отдельно тесты с реальными данными. На границе системы выполните явный разбор, например int(raw_value), и решите, как сообщать о некорректном вводе.

Тренировка: среднее с отсутствующим результатом

Опишите функцию average(scores: list[float]) -> float | None: для пустого списка она возвращает None, иначе среднее значение. Затем напишите format_average, которая принимает этот результат и печатает нет оценок либо число с одной цифрой после точки. Проверьте [] и [4.0, 5.0, 3.0].

Самостоятельная проверка: намеренно вызовите average(['5']) и сравните предупреждение анализатора с ошибкой исполнения. Исправьте данные явным преобразованием до вызова. Добавьте переменную groups: dict[str, list[float]] и убедитесь, что выбранная аннотация точно отражает два уровня структуры.

Типы помогают, проверки защищают

Подсказка типа документирует контракт и даёт инструментам материал для проверки, но не заменяет валидацию. Параметризованные коллекции описывают элементы и ключи, объединение перечисляет допустимые варианты, а None требует явной ветви. Сужение типа должно следовать из реальной проверки, которая также защищает выполнение программы.

Аннотации и проверка во время выполнения

Проверяет ли Python аннотации при каждом вызове?

Нет. Интерпретатор обычно сохраняет их как метаданные. Проверку выполняют отдельные статические инструменты или специально подключённые библиотеки времени исполнения.

Что выбрать: str | None или Optional[str]?

Для Python 3.10 и новее str | None короче и читается прямо. Optional[str] из typing выражает тот же набор вариантов и нужен в проектах с иными соглашениями или старой целевой версией.

Следует ли использовать Any для сложных данных?

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

Источники