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 hora — AttributeError: 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/StrEnumpara 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 umstr, esse é o sinal de que deveria ser umEnum. - Prefira
StrEnumaEnumcomum quando o valor precisa trafegar como string (JSON, API, banco de dados) — evita o boilerplate de conversão manual e a "gambiarra" doclass X(str, Enum). - Use
Flagpara 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
IntEnumsó quando a ordem numérica for semanticamente relevante — para status sem hierarquia natural, umEnum/StrEnumcomum é mais claro. - Centralize os Enums do domínio em um módulo próprio (ex:
enums.pyou 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.