Enums em Python: pare de usar string solta para representar estado

Enums em Python: pare de usar string solta para representar estado

Todo sistema tem campos que só podem assumir um conjunto fixo de valores: status de um pedido ("pendente", "pago", "cancelado"), papel de um usuário ("admin", "editor", "leitor"), nível de log ("info", "warning", "error"). E é surpreendentemente comum ver esses valores representados como string solta, espalhada pelo código:

def processar_pedido(status: str) -> None:
    if status == "pago":
        ...
    elif status == "pendete":  # erro de digitação — ninguém avisa
        ...

Esse "pendete" com erro de digitação não vai gerar nenhum erro de sintaxe, nenhum aviso do editor, nenhuma falha nos testes automatizados — a não ser que exista um teste específico cobrindo exatamente esse caminho. Ele simplesmente vai fazer o if nunca ser verdadeiro, silenciosamente, até alguém notar que pedidos pendentes não estão sendo processados corretamente.

É esse tipo de problema que o Enum resolve.

O que é um Enum

Enum, da biblioteca padrão do Python, permite declarar um conjunto fechado e nomeado de valores possíveis:

from enum import Enum

class StatusPedido(Enum):
    PENDENTE = "pendente"
    PAGO = "pago"
    CANCELADO = "cancelado"

def processar_pedido(status: StatusPedido) -> None:
    if status == StatusPedido.PAGO:
        ...
    elif status == StatusPedido.PENDENTE:
        ...

Agora, se alguém digitar StatusPedido.PENDETE por engano, o erro aparece na horaAttributeError: PENDETE, ou, se você estiver usando mypy/pyright, o erro aparece ainda antes, direto no editor, sem nem precisar rodar o código.

Essa é a primeira vantagem prática do Enum sobre string solta: o conjunto de valores válidos passa a ser verificável, tanto em tempo de execução quanto estaticamente.

Por que não só "usar uma constante"?

Uma resposta comum de quem já sofreu com string solta é: "então uso constantes":

STATUS_PENDENTE = "pendente"
STATUS_PAGO = "pago"
STATUS_CANCELADO = "cancelado"

Isso já é uma melhoria — pelo menos o erro de digitação vira um NameError claro. Mas ainda falta o que o Enum traz de verdade: agrupamento semântico e um tipo próprio. Com constantes soltas, nada impede que STATUS_PENDENTE seja comparado, por engano, com uma constante de outro domínio completamente diferente que também vale "pendente". Com Enum, StatusPedido.PENDENTE só é igual a outro membro do mesmo StatusPedido — o tipo carrega o contexto junto com o valor.

Iterando, comparando e usando em estruturas

Um Enum se comporta bem em situações comuns do dia a dia:

# Iterar por todos os valores possíveis
for status in StatusPedido:
    print(status.name, status.value)
# PENDENTE pendente
# PAGO pago
# CANCELADO cancelado

# Comparação direta
StatusPedido.PAGO == StatusPedido.PAGO  # True

# Uso como chave de dicionário — funciona normalmente, é hashable
regras_transicao = {
    StatusPedido.PENDENTE: [StatusPedido.PAGO, StatusPedido.CANCELADO],
    StatusPedido.PAGO: [],
}

Isso já resolve boa parte do que motivava usar string solta, mas com a segurança extra de um tipo fechado.

StrEnum: o melhor dos dois mundos (Python 3.11+)

Um problema prático do Enum "clássico" aparece na hora de serializar para JSON ou comparar diretamente com uma string vinda de fora (de uma API, por exemplo):

import json

json.dumps({"status": StatusPedido.PAGO})
# TypeError: Object of type StatusPedido is not JSON serializable

Antes do Python 3.11, a solução comum era criar um Enum que também herdasse de str:

class StatusPedido(str, Enum):
    PENDENTE = "pendente"
    PAGO = "pago"

Isso funcionava, mas era um pouco de "gambiarra elegante" — misturar herança de str com Enum tem efeitos colaterais sutis na formatação (str() e repr() se comportam de forma inconsistente entre versões).

A partir do Python 3.11, existe StrEnum, feito exatamente para isso, sem os efeitos colaterais:

from enum import StrEnum

class StatusPedido(StrEnum):
    PENDENTE = "pendente"
    PAGO = "pago"
    CANCELADO = "cancelado"

StatusPedido.PAGO == "pago"  # True
json.dumps({"status": StatusPedido.PAGO})
# '{"status": "pago"}'

Com StrEnum, o membro se comporta como uma string de verdade em qualquer contexto que espere uma — comparação, serialização, formatação — mas continua sendo um Enum para efeitos de agrupamento, iteração e checagem estática. Para quase todo caso de "status" ou "categoria" representado como string, StrEnum é hoje a escolha recomendada em projetos que já rodam em 3.11+.

Enum com valores automáticos: auto()

Quando o valor exato não importa — só a distinção entre os membros —, dá para deixar o Python gerar os valores automaticamente:

from enum import Enum, auto

class Prioridade(Enum):
    BAIXA = auto()
    MEDIA = auto()
    ALTA = auto()

print(Prioridade.MEDIA.value)  # 2

Isso é útil em enums internos, onde o valor numérico nunca é exposto para fora do sistema (não vai para uma API, não é persistido como está). Se o valor for persistido em banco de dados ou trafegar em uma API, é mais seguro declarar os valores explicitamente — auto() pode mudar de resultado se a ordem dos membros for alterada, o que quebraria dados já salvos.

Flag: quando um valor pode combinar múltiplas opções

Existe uma categoria de problema diferente: permissões que podem ser combinadas. Um usuário pode ter permissão de leitura, escrita, ambas, ou nenhuma — não é um valor único entre opções exclusivas, é uma combinação de bits.

Fazer isso "na mão" costuma envolver operações bit a bit direto com inteiros, o que é eficiente mas ilegível:

LEITURA = 1
ESCRITA = 2
EXECUCAO = 4

permissoes = LEITURA | ESCRITA  # 3 — o que isso significa? não é óbvio lendo o código

O Flag resolve isso mantendo a mesma eficiência (por baixo, ainda são bits), mas com legibilidade de Enum:

from enum import Flag, auto

class Permissao(Flag):
    LEITURA = auto()
    ESCRITA = auto()
    EXECUCAO = auto()

permissoes = Permissao.LEITURA | Permissao.ESCRITA

Permissao.LEITURA in permissoes   # True
Permissao.EXECUCAO in permissoes  # False

print(permissoes)
# Permissao.LEITURA|ESCRITA

Isso é exatamente o tipo de estrutura usada, por exemplo, para representar permissões de arquivo (leitura/escrita/execução, como no Unix) ou flags de configuração combináveis — e fica muito mais legível e seguro do que manipular inteiros com operadores bit a bit espalhados pelo código.

IntEnum: quando a ordem numérica importa

Existe ainda o IntEnum, para casos onde os membros precisam se comportar como inteiros de verdade, inclusive em comparações de ordem:

from enum import IntEnum

class Nivel(IntEnum):
    BAIXO = 1
    MEDIO = 2
    ALTO = 3

Nivel.ALTO > Nivel.BAIXO  # True — comparação numérica direta

Isso é útil para níveis hierárquicos (prioridade, severidade, permissão em camadas) onde faz sentido perguntar "esse nível é maior que aquele?". Um Enum comum não permite comparação de ordem por padrão — só igualdade.

Enum em modelos Pydantic

Conectando com os artigos anteriores da série: o Pydantic v2 valida Enum nativamente, sem esforço extra:

from pydantic import BaseModel

class Pedido(BaseModel):
    status: StatusPedido

Pedido(status="pago")   # ✅ convertido automaticamente para StatusPedido.PAGO
Pedido(status="invalido")
# ValidationError: Input should be 'pendente', 'pago' or 'cancelado'

Repare que a mensagem de erro já lista os valores válidos automaticamente — outro ganho de usar Enum em vez de str solta: a própria validação já documenta, no erro, quais são as opções aceitas.

Boas práticas

  • Use Enum/StrEnum para qualquer campo com conjunto fechado de valores possíveis — status, categoria, papel de usuário, nível de log. Se você já escreveu um comentário do tipo # valores possíveis: "a", "b" ou "c" ao lado de um str, esse é o sinal de que deveria ser um Enum.
  • Prefira StrEnum a Enum comum quando o valor precisa trafegar como string (JSON, API, banco de dados) — evita o boilerplate de conversão manual e a "gambiarra" do class X(str, Enum).
  • Use Flag para combinações de opções, não para status mutuamente exclusivos — se só um valor pode estar ativo por vez, é Enum; se vários podem coexistir, é Flag.
  • Evite auto() para valores que serão persistidos ou trafegados externamente — declare os valores explicitamente, para não depender da ordem de declaração dos membros.
  • Use IntEnum só quando a ordem numérica for semanticamente relevante — para status sem hierarquia natural, um Enum/StrEnum comum é mais claro.
  • Centralize os Enums do domínio em um módulo próprio (ex: enums.py ou dentro de cada app, em projetos Django) — evita duplicação da mesma lista de valores em lugares diferentes do sistema.

Conclusão

String solta para representar estado é um daqueles atalhos que parecem inofensivos no primeiro protótipo, mas cobram o preço depois — em erro de digitação silencioso, em comparação com valor de outro domínio, em falta de documentação de quais valores são realmente válidos. Enum resolve isso sem custo real de complexidade: é biblioteca padrão, tem sintaxe simples, e — com StrEnum — se comporta como string em qualquer lugar que precise.

A regra prática: toda vez que você digitar uma string entre aspas pela segunda vez para representar o mesmo "tipo de coisa" — outro status, outra categoria, outro papel —, é hora de parar e criar um Enum. É menos código no fim das contas, não mais.