Короткий ответ
enum.Enum даёт самый строгий контроль и единый источник истины в runtime, но каждое добавление нового статуса требует релиза общей библиотеки и обновления всех её потребителей; typing.Literal даёт схожую строгость только на уровне статической проверки типов (mypy/pyright) без runtime-гарантий и без накладных расходов на объект enum; простые строковые константы самые гибкие и дешёвые для расширения, но не защищают ни во время выполнения, ни статически от опечаток и несогласованных значений между сервисами.
Как это работает подробнее
Enum — это настоящий класс с конечным, явно перечисленным в коде набором членов; проверка принадлежности (status in OrderStatus) и исчерпывающий перебор (list(OrderStatus)) работают в runtime, но любое новое значение статуса должно появиться как новый член enum именно в этом коде, то есть требует релиза библиотеки, где enum объявлен, и обновления зависимости везде, где он используется. Literal["pending", "paid", ...] существует только для статических анализаторов типов — в runtime это обычная строка без каких-либо проверок, поэтому неверное значение не будет отловлено во время выполнения, только при статической проверке типов (и только там, где такая проверка реально подключена в CI). Обычные строковые константы (STATUS_PENDING = "pending") — это просто более читаемые имена для строковых литералов без какой-либо встроенной защиты ни на одном из уровней.
Пример кода
from enum import Enum
from typing import Literal
class PaymentStatus(Enum): # строгий runtime-контроль, но требует релиза при расширении
PENDING = "pending"
PAID = "paid"
FAILED = "failed"
KnownStatus = Literal["pending", "paid", "failed"] # только статическая подсказка, без runtime-проверки
def validate_status_from_db(raw: str) -> str:
# явная граница системы: валидация независимо от того, какой подход выбран для типов
known = {s.value for s in PaymentStatus}
if raw not in known:
raise ValueError(f"Неизвестный статус платежа из БД: {raw!r}")
return rawЧто использовать на практике
- Практика. Если разные команды должны самостоятельно и часто добавлять новые статусы без скоординированного релиза общей библиотеки у всех потребителей, строгий
Enumв общей библиотеке становится операционным узким местом — каждое добавление статуса блокируется на релиз и обновление зависимости у всех. В этом случае практичным компромиссом может быть менее строгая runtime-модель (строка + документированный, но не являющийся исчерчерпывающим enum список "известных на этот момент" значений для автодополнения и статической проверки черезLiteralтам, где это осмысленно) в сочетании с явной валидацией на границах системы (при записи в БД, при приёме запроса), а не полной enum-защитой во всех внутренних точках кода.
Подводные камни
- Риски. Выбирают
Enum"потому что он самый правильный" без учёта операционной стоимости релизов общей библиотеки при каждом новом статусе, что на практике приводит либо к задержкам в поставке фич командами-потребителями, либо к обходным путям (хранению статуса как произвольной строки "временно", что убивает весь смысл enum). Также полагаются только наLiteralи статическую проверку типов как единственную защиту, забывая, что любые данные, пришедшие из внешнего источника (БД, HTTP-запрос, сообщение из очереди) не проверяются типизатором в runtime и могут содержать произвольное значение, если нет отдельной явной валидации. - Компромисс. Чем строже механизм (Enum), тем выше гарантии согласованности, но выше стоимость изменения набора значений и координации между командами; чем мягче механизм (строка/Literal), тем быстрее и дешевле расширение, но выше риск расхождения значений между сервисами и отсутствие runtime-защиты. Выбор зависит от того, как часто ожидается добавление новых статусов, сколько команд должно иметь возможность делать это независимо, и насколько критичны последствия рассинхронизации значений статуса между сервисами для бизнес-процесса (например, для платежей ошибка в статусе намного дороже, чем для внутреннего технического лейбла).