overload в Python: тип результата зависит от аргумента

Функция находит запись по номеру. Обычно отсутствие записи возвращает None, но режим required должен выдавать ошибку. Общий тип результата Record | None заставляет проверять None даже там, где required=True уже гарантирует запись.

Связать флаг и обещание функции

Перегрузки typing.overload описывают несколько вариантов вызова для статического анализатора. Реализация при этом одна. Сигнатура с Literal[True] обещает Record, вариант с False — Record | None. У вызывающего кода появляется точная информация, соответствующая выбранному режиму.

Функция обязана выполнить это обещание во время работы. Если запись отсутствует и required=True, она поднимает LookupError. Возврат None в этой ветке нарушил бы аннотацию, даже если отдельный вызов в тесте не упал. Поэтому проверяем не только найденную запись, но и обе ветки отсутствия.

Оставить вариант для переменного bool

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

Пример самодостаточен. Его можно выполнить Python и отдельно проверить mypy. Исполнение проверяет ветки функции; статическая проверка отвечает за согласованность описанных вариантов. Ни одна из этих проверок не подменяет другую.

from dataclasses import dataclass
from typing import Literal, overload

@dataclass(frozen=True)
class Record:
    name: str

@overload
def find(key: int, *, required: Literal[True]) -> Record: ...
@overload
def find(key: int, *, required: Literal[False] = False) -> Record | None: ...
@overload
def find(key: int, *, required: bool) -> Record | None: ...
def find(key: int, *, required: bool = False) -> Record | None:
    record = {1: Record("alpha")}.get(key)
    if record is None and required:
        raise LookupError(key)
    return record

assert find(1, required=True).name == "alpha"
assert find(2) is None
try:
    find(2, required=True)
except LookupError:
    pass
else:
    raise AssertionError("required record missing")
print("lookup modes checked")

Когда перегрузки усложняют API

Если вариантов становится слишком много, потребителю трудно помнить связь флагов. Иногда две функции find и get_required понятнее одной перегруженной. overload полезен там, где варианты действительно выражают один механизм и часто используются вместе. Количество сигнатур не должно компенсировать непонятные имена аргументов.

Не пишите несколько обычных def с одним именем: Python сохранит только последнюю. Декораторы overload предназначены для типового описания перед реализацией. Во время выполнения вызывается итоговая функция, а не подходящая по типам заглушка.

Потренировать точные аннотации

В открытом уроке type hints и mypy начните с типов аргументов и результата. Затем проверьте описанный поиск с двумя режимами. Посмотрите программу курса: Optional, Union и Literal дают основу для таких сигнатур.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

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