В Python добавили команду, но забыли обработчик: проверка через assert_never

У обработчика команд есть два допустимых действия: создать задачу и закрыть её. Пока вариантов мало, цепочка if выглядит законченной. Потом в тип добавляют archive, правят парсер и интерфейс, а старый обработчик остаётся без новой ветки. Код продолжает запускаться, но доходит до конца функции без результата. Ошибка появляется далеко от изменения, которое её вызвало.

Обычный else с ValueError помогает при выполнении, однако не сообщает автору новой команды, какие функции ещё нужно обновить. Для конечного набора вариантов полезнее оставить в последней ветке assert_never. Этот вызов говорит проверяющему типы: сюда не должно оставаться ни одного допустимого значения. Если осталось, разбор неполон. В Python 3.11 функция доступна в typing; ниже используем Python 3.12.

Закрытый набор команд

Начнём с обработчика, в котором все варианты разобраны. Важна аннотация аргумента: если оставить str, проверяющий не сможет доказать, что две строки исчерпывают множество. Literal описывает закрытый набор команд, а не произвольный пользовательский текст. Преобразование внешней строки в такой набор остаётся отдельной задачей парсера.

from typing import Literal, assert_never

Command = Literal["create", "close"]

def handle(command: Command) -> str:
    if command == "create":
        return "created"
    if command == "close":
        return "closed"
    assert_never(command)

assert handle("close") == "closed"
print(handle("create"))

Проверка после расширения модели

Сохраните пример как command.py и выполните python command.py. Получится created. Затем установите проверяющий командой python -m pip install mypy и запустите python -m mypy --strict command.py. Начальный вариант проходит. Теперь добавьте "archive" в Literal, не меняя тело функции: mypy укажет на аргумент assert_never, потому что в этой точке остаётся Literal['archive'].

Не заменяйте последнюю строку на assert False. Она остановит программу, если выполнение туда попадёт, но не выражает тот же контракт для анализа исчерпывающего разбора. Не стоит и приводить command к Never через cast: это скрывает как раз ту информацию, ради которой добавлена проверка. Исправление состоит в новой содержательной ветке и тесте её результата.

Есть практическая разница между обработкой неизвестного ввода и пропущенным внутренним вариантом. На входе приложения неизвестную строку нужно отклонить понятной ошибкой. Внутри типизированной модели неожиданная команда означает рассогласование кода. Смешав эти случаи в одном широком else, легко заставить новый допустимый сценарий выглядеть как ошибка пользователя.

Такой приём особенно полезен при изменении форматов событий и небольших наборов состояний. Найдите функции, которые разбирают один и тот же союз типов, и добавьте проверку в каждую. После расширения модели список диагностик станет списком мест для доработки. В проверку изменений включите и mypy, и запуск тестов: первый следит за полнотой веток, вторые — за смыслом результата.

Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.

Посмотреть программу и практику курса.

Документация: официальный источник по теме.

Другие задачи по теме: Типы, тесты и поведение программы.

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

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

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