Enum em Python: Enum, StrEnum, IntEnum, Flag e auto
Guia prático de enum em Python: Enum, StrEnum (3.11), IntEnum, Flag, auto e boas práticas com dataclasses, Pydantic e FastAPI em português brasileiro.
O módulo enum da biblioteca padrão do Python resolve um problema que aparece em todo sistema real: valores finitos com significado de negócio — status de pedido, tipo de documento, permissão, canal de notificação. Em vez de espalhar "pendente", "pago" e "cancelado" como strings mágicas (ou 1, 2, 3 como inteiros opacos), você declara um tipo fechado, legível em stack traces e seguro para o type checker.
Este guia cobre Enum, StrEnum (Python 3.11+), IntEnum, Flag/IntFlag, auto(), iteração, comparação, serialização e o encaixe com dataclasses, Pydantic, FastAPI e pattern matching. Complementa tipagem estática com mypy, boas práticas Python 2026 e o trio funcional da stdlib (collections, itertools, functools). A meta não é listar cada método da API: é saber qual variante usar e onde o enum paga o custo cognitivo.
Por que não basta uma string ou um int?
Três falhas clássicas do código “sem enum”:
- Typos silenciosos —
"pendete"passa em runtime e só aparece no relatório de cliente. - Autocomplete zero — o editor não sugere os estados válidos do domínio.
- Comparação frouxa —
"Pago" == "pago"falha;1e"1"se misturam em planilhas e CSVs brasileiros.
# Frágil: strings mágicas
def pode_enviar(status: str) -> bool:
return status in {"pago", "aprovado"} # e se vier "PAGO"?
# Robusto: tipo fechado
from enum import Enum
class StatusPedido(Enum):
PENDENTE = "pendente"
PAGO = "pago"
APROVADO = "aprovado"
ENVIADO = "enviado"
CANCELADO = "cancelado"
def pode_enviar(status: StatusPedido) -> bool:
return status in {StatusPedido.PAGO, StatusPedido.APROVADO}
Com o enum, o type checker e o IDE avisam se você passar um valor inválido. Em code review, StatusPedido.CANCELADO comunica intenção melhor que "cancelado" solto no meio de um pipeline ETL.
Enum básico: membros, name e value
from enum import Enum
class CanalNotificacao(Enum):
EMAIL = "email"
SMS = "sms"
PUSH = "push"
WHATSAPP = "whatsapp"
print(CanalNotificacao.EMAIL)
# CanalNotificacao.EMAIL
print(CanalNotificacao.EMAIL.name) # "EMAIL"
print(CanalNotificacao.EMAIL.value) # "email"
# Lookup pelo valor:
print(CanalNotificacao("whatsapp"))
# CanalNotificacao.WHATSAPP
# Lookup pelo nome:
print(CanalNotificacao["SMS"])
# CanalNotificacao.SMS
Regras que importam no dia a dia:
- Membros são singletons —
CanalNotificacao.EMAIL is CanalNotificacao("email")éTrue. - Iteração devolve os membros na ordem de declaração.
- Comparação entre membros do mesmo enum usa identidade; comparar com o valor cru (
== "email") falha noEnumclássico. - Hashável — serve como chave de
dicte entra emset(útil com Counter e defaultdict).
for canal in CanalNotificacao:
print(canal.name, "→", canal.value)
rotulos = {
CanalNotificacao.EMAIL: "E-mail",
CanalNotificacao.WHATSAPP: "WhatsApp Business",
}
Valores inválidos levantam ValueError
try:
CanalNotificacao("telegram")
except ValueError as exc:
print(exc)
# 'telegram' is not a valid CanalNotificacao
Na borda da aplicação (JSON de webhook, coluna de planilha, query string), capture ValueError e devolva 400 / mensagem amigável — o mesmo padrão de validação com Pydantic.
StrEnum (Python 3.11+): o default moderno para APIs
A dor histórica do Enum com valor string é precisar de .value em todo lugar: logs, templates, JSON, comparação com dados externos. O StrEnum resolve isso herdando de str e de Enum:
from enum import StrEnum, auto
class StatusPedido(StrEnum):
PENDENTE = "pendente"
PAGO = "pago"
ENVIADO = "enviado"
ENTREGUE = "entregue"
CANCELADO = "cancelado"
status = StatusPedido.PAGO
print(status == "pago") # True ← diferença crucial
print(f"Pedido {status}") # Pedido pago
print(status.upper()) # PAGO ← métodos de str funcionam
print(isinstance(status, str)) # True
print(isinstance(status, StatusPedido)) # True
auto() com StrEnum
Em StrEnum, auto() usa o nome do membro em minúsculas como valor — excelente para códigos estáveis sem digitar duas vezes:
from enum import StrEnum, auto
class TipoDocumento(StrEnum):
CPF = auto() # "cpf"
CNPJ = auto() # "cnpj"
NFE = auto() # "nfe"
BOLETO = auto() # "boleto"
assert TipoDocumento.CNPJ == "cnpj"
Para domínios brasileiros (CPF/CNPJ, NF-e, boleto, PIX), StrEnum + auto() (ou valores explícitos em português sem acento no value) evita o drift entre banco, fila e frontend.
Quando NÃO usar StrEnum
- O valor canônico não é texto (use
IntEnumouEnumcom tuplas). - Você precisa que
Status.ATIVO == "ativo"seja False de propósito (menos comum). - Precisa rodar em Python anterior ao 3.11 — aí use o padrão clássico
class Status(str, Enum):(ver seção de compatibilidade).
IntEnum e contadores estáveis
IntEnum faz o membro se comportar como int. Útil para códigos numéricos de legado, colunas de banco antigas e protocolos binários:
from enum import IntEnum
class HttpStatusFamilia(IntEnum):
INFORMACIONAL = 100
SUCESSO = 200
REDIRECIONAMENTO = 300
ERRO_CLIENTE = 400
ERRO_SERVIDOR = 500
print(HttpStatusFamilia.SUCESSO == 200) # True
print(HttpStatusFamilia.SUCESSO + 1) # 201
print(sorted(HttpStatusFamilia)) # ordena como int
Cuidado: IntEnum mistura com inteiros em comparações. Isso é prático e perigoso — um if status == 200 passa sem deixar claro que o domínio é HTTP. Prefira Enum/StrEnum quando o número for só um detalhe de serialização.
Flag e IntFlag: combinações de bits
Quando o usuário pode ter várias permissões ao mesmo tempo, Flag é a ferramenta certa:
from enum import Flag, auto
class Permissao(Flag):
LER = auto()
ESCREVER = auto()
EXECUTAR = auto()
ADMIN = LER | ESCREVER | EXECUTAR
alice = Permissao.LER | Permissao.ESCREVER
print(alice) # Permissao.LER|ESCREVER
print(Permissao.LER in alice) # True
print(bool(alice & Permissao.EXECUTAR)) # False
# Revogar escrita:
alice = alice & ~Permissao.ESCREVER
print(alice) # Permissao.LER
IntFlag é o irmão que também se comporta como int (útil se o banco grava a máscara numérica). Em feature flags de produto, combine com o guia de feature flags em Python — o enum modela o conjunto de flags conhecidas; o store (Redis, config) decide o que está ligado para cada tenant.
auto(), aliases e unicidade
from enum import Enum, auto, unique
@unique # impede valores duplicados (aliases)
class CorSemaforo(Enum):
VERMELHO = auto() # 1
AMARELO = auto() # 2
VERDE = auto() # 3
print(list(CorSemaforo))
Sem @unique, dois nomes com o mesmo valor criam um alias (o segundo nome aponta para o primeiro membro). Isso é útil para renomear com compatibilidade (OK = SUCCESS = auto() com valor compartilhado via atribuição explícita), mas confunde se for acidente.
Para controlar a geração de auto():
from enum import Enum, auto
class Prioridade(Enum):
def _generate_next_value_(name, start, count, last_values):
# Valores 10, 20, 30… em vez de 1, 2, 3
return (count + 1) * 10
BAIXA = auto() # 10
MEDIA = auto() # 20
ALTA = auto() # 30
CRITICA = auto() # 40
Métodos e propriedades no enum
Enums são classes: você pode adicionar helpers de domínio sem criar um service à parte.
from enum import StrEnum
class StatusPedido(StrEnum):
PENDENTE = "pendente"
PAGO = "pago"
ENVIADO = "enviado"
ENTREGUE = "entregue"
CANCELADO = "cancelado"
@property
def eh_final(self) -> bool:
return self in {StatusPedido.ENTREGUE, StatusPedido.CANCELADO}
def pode_transicionar_para(self, novo: "StatusPedido") -> bool:
fluxos = {
StatusPedido.PENDENTE: {StatusPedido.PAGO, StatusPedido.CANCELADO},
StatusPedido.PAGO: {StatusPedido.ENVIADO, StatusPedido.CANCELADO},
StatusPedido.ENVIADO: {StatusPedido.ENTREGUE},
StatusPedido.ENTREGUE: set(),
StatusPedido.CANCELADO: set(),
}
return novo in fluxos[self]
assert StatusPedido.PAGO.pode_transicionar_para(StatusPedido.ENVIADO)
assert not StatusPedido.CANCELADO.pode_transicionar_para(StatusPedido.PAGO)
Isso encaixa bem em máquinas de estado simples e em testes com pytest — a regra de negócio vive perto do tipo.
Dataclass + Enum
O exemplo canônico de pedido que já aparece no guia de dataclasses fica mais claro com StrEnum:
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
class StatusPedido(StrEnum):
PENDENTE = "pendente"
PAGO = "pago"
ENVIADO = "enviado"
ENTREGUE = "entregue"
@dataclass
class ItemPedido:
produto: str
quantidade: int
preco_unitario: float
@property
def subtotal(self) -> float:
return self.quantidade * self.preco_unitario
@dataclass
class Pedido:
cliente: str
itens: list[ItemPedido] = field(default_factory=list)
status: StatusPedido = StatusPedido.PENDENTE
criado_em: datetime = field(default_factory=datetime.now)
@property
def total(self) -> float:
return sum(item.subtotal for item in self.itens)
pedido = Pedido(cliente="Maria Silva")
pedido.status = StatusPedido.PAGO
print(pedido.status, pedido.status == "pago") # pago True
Com slots=True e frozen=True nas dataclasses imutáveis, o enum como campo continua hashável e seguro para caches — combine com lru_cache do functools só em funções puras.
Pydantic e FastAPI
Pydantic entende Enum/StrEnum nativamente: valida entrada, documenta o enum no OpenAPI e serializa o valor.
from enum import StrEnum
from pydantic import BaseModel, Field
class TipoChavePix(StrEnum):
CPF = "cpf"
CNPJ = "cnpj"
EMAIL = "email"
TELEFONE = "telefone"
ALEATORIA = "aleatoria"
class CobrancaPix(BaseModel):
valor_centavos: int = Field(gt=0)
tipo_chave: TipoChavePix
chave: str
cobranca = CobrancaPix(
valor_centavos=1500,
tipo_chave="email", # aceito e convertido para TipoChavePix.EMAIL
chave="[email protected]",
)
print(cobranca.model_dump())
# {'valor_centavos': 1500, 'tipo_chave': <TipoChavePix.EMAIL: 'email'>, ...}
print(cobranca.model_dump(mode="json"))
# tipo_chave vira "email" no JSON
Em FastAPI, o mesmo tipo vira dropdown no Swagger e rejeita valores fora da lista — ideal para APIs REST e webhooks de CRM. Para pagamentos, veja também a integração PIX com Python.
from fastapi import FastAPI
from pydantic import BaseModel
from enum import StrEnum
app = FastAPI()
class Ambiente(StrEnum):
HOMOLOG = "homolog"
PRODUCAO = "producao"
class ConfigNotificacao(BaseModel):
ambiente: Ambiente
canal: str
@app.post("/config")
def salvar_config(body: ConfigNotificacao) -> dict:
return {"ok": True, "ambiente": body.ambiente}
Pattern matching (match/case)
A partir do Python 3.10, match combina de forma limpa com enums — sem cascata de if/elif e sem strings mágicas:
from enum import StrEnum
class EventoPedido(StrEnum):
CRIADO = "criado"
PAGO = "pago"
ENVIADO = "enviado"
CANCELADO = "cancelado"
def mensagem_cliente(evento: EventoPedido) -> str:
match evento:
case EventoPedido.CRIADO:
return "Recebemos o seu pedido."
case EventoPedido.PAGO:
return "Pagamento confirmado. Vamos separar o estoque."
case EventoPedido.ENVIADO:
return "Seu pedido saiu para entrega."
case EventoPedido.CANCELADO:
return "Pedido cancelado. Se pagou, o estorno segue a política."
case _:
return "Atualização registrada."
Detalhes e armadilhas de esgotamento de padrões estão no guia de pattern matching com match/case.
Serialização: JSON, CSV, banco e logs
| Destino | Enum clássico | StrEnum / IntEnum |
|---|---|---|
json.dumps | precisa de default/ .value | StrEnum serializa como str; IntEnum como int |
| CSV / Excel | grave .value | idem; no Brasil use ; e encoding corretos (CSV) |
| SQL | coluna VARCHAR/INT + check | ORM mapeia o enum; prefira value estável |
| Logs | f"{membro}" → Status.PAGO | StrEnum loga o value; ou use .name de propósito |
import json
from enum import Enum, StrEnum
class Legado(Enum):
ATIVO = "ativo"
class Moderno(StrEnum):
ATIVO = "ativo"
# Legado: TypeError sem default
print(json.dumps({"s": Moderno.ATIVO}))
# {"s": "ativo"}
print(json.dumps({"s": Legado.ATIVO}, default=lambda o: o.value))
# {"s": "ativo"}
Nunca persista str(Legado.ATIVO) se o formato for "Legado.ATIVO" — na leitura você não reconstrói o membro com Legado("Legado.ATIVO"). Grave value (ou name, se essa for a convenção documentada do time) e leia com Legado(...) / Legado[name].
Compatibilidade: Python anterior ao 3.11
Sem StrEnum, o padrão idiomático é herança múltipla com str:
from enum import Enum
class StatusPedido(str, Enum):
PENDENTE = "pendente"
PAGO = "pago"
Comportamento próximo ao StrEnum, com diferenças sutis de subclassing documentadas no PEP 663. Em código novo com 3.11+, prefira StrEnum — a intenção fica explícita e o auto() gera value a partir do nome.
Para detectar a versão em runtime (útil em libs):
import sys
from enum import Enum
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
class StrEnum(str, Enum):
"""Polyfill mínimo para 3.10."""
Enum × Literal × constantes de módulo
| Ferramenta | Quando usar |
|---|---|
Enum / StrEnum | Conjunto nomeado, iterável, com métodos de domínio, documentado no OpenAPI |
Literal["a", "b"] | Tipagem estreita em funções sem runtime enum (mypy/pyright) |
Constantes STATUS_PAGO = "pago" | Scripts curtos; evita se o conjunto cresce |
Literal e enum se complementam: você pode anotar status: StatusPedido e ainda usar Literal em APIs internas menores. Para times que adotam ty / mypy / Pyrefly, o enum fecha o conjunto em runtime e em análise estática.
Erros comuns (e como evitar)
Comparar Enum clássico com string e achar que é bug do Python. Status.PAGO == "pago" é False por design. Migre para StrEnum ou compare com o membro / .value de forma consciente.
Usar print(status) em log e persistir "Status.PAGO". Separe representação de debug (.name) de valor de negócio (.value).
Mutar .value. Membros de enum não devem ser reassignados em runtime; trate-os como constantes.
Enum gigante com 200 membros “só por organização”. Se o conjunto muda toda semana (lista de municípios, catálogo de produtos), use tabela/banco ou arquivo de dados — enum é para conjuntos estáveis.
Flag onde o estado é exclusivo. PENDENTE | PAGO não faz sentido semântico; use Enum simples e uma máquina de estados.
Importar o pacote PyPI enum. Em ambientes legados isso mascara a stdlib. Use sempre a biblioteca padrão.
Checklist rápido
- Estado exclusivo com valor texto de API? →
StrEnum(3.11+) ouclass X(str, Enum). - Código numérico de legado/protocolo? →
IntEnum. - Várias opções combináveis? →
Flag/IntFlag. - Preciso de métodos de domínio (transições, labels)? → métodos/
@propertyno próprio enum. - Validação de entrada HTTP/JSON? → Pydantic + enum no campo.
- Conjunto instável ou enorme? → não use enum; use dados.
Conclusão
O enum é uma das peças da stdlib com melhor retorno em código de produto: elimina strings mágicas, documenta o domínio, melhora autocomplete e encaixa em dataclasses, Pydantic, FastAPI e match/case. Em 2026, o default sensato para APIs e sistemas brasileiros é StrEnum (ou str, Enum em 3.10); reserve IntEnum para códigos numéricos e Flag para permissões e máscaras.
Combine com dataclasses, Pydantic, pattern matching, tipagem estática e as ferramentas de funções da stdlib (functools, itertools) para modelar o domínio sem framework extra. Na dúvida, troque o próximo "pendente" espalhado no código por um Status.PENDENTE — o type checker e o eu-do-futuro agradecem no mesmo PR.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português