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.

11 min de leitura Equipe Python Brasil

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”:

  1. Typos silenciosos"pendete" passa em runtime e só aparece no relatório de cliente.
  2. Autocomplete zero — o editor não sugere os estados válidos do domínio.
  3. Comparação frouxa"Pago" == "pago" falha; 1 e "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 singletonsCanalNotificacao.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 no Enum clássico.
  • Hashável — serve como chave de dict e entra em set (ú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 IntEnum ou Enum com 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

DestinoEnum clássicoStrEnum / IntEnum
json.dumpsprecisa de default/ .valueStrEnum serializa como str; IntEnum como int
CSV / Excelgrave .valueidem; no Brasil use ; e encoding corretos (CSV)
SQLcoluna VARCHAR/INT + checkORM mapeia o enum; prefira value estável
Logsf"{membro}"Status.PAGOStrEnum 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

FerramentaQuando usar
Enum / StrEnumConjunto 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

  1. Estado exclusivo com valor texto de API?StrEnum (3.11+) ou class X(str, Enum).
  2. Código numérico de legado/protocolo?IntEnum.
  3. Várias opções combináveis?Flag / IntFlag.
  4. Preciso de métodos de domínio (transições, labels)? → métodos/@property no próprio enum.
  5. Validação de entrada HTTP/JSON? → Pydantic + enum no campo.
  6. 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.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português