uuid em Python: identificadores únicos para APIs, banco e logs
Aprenda a usar o módulo uuid em Python para gerar IDs únicos em APIs, bancos e logs, escolher entre UUID4, UUID7 e UUID5 e evitar erros comuns.
O módulo uuid é a forma padrão de gerar identificadores únicos em Python para pedidos, usuários públicos, correlação de logs, chaves de API de negócio e chaves primárias. Ele implementa o padrão RFC 4122 e, nas versões recentes, também as variantes mais novas usadas em sistemas distribuídos.
A recomendação direta é: use uuid.uuid4() quando quiser um ID aleatório simples e portátil; prefira uuid.uuid7() quando a ordem temporal e o desempenho de índice importarem; use uuid.uuid5() quando o mesmo dado de entrada deve produzir sempre o mesmo identificador. Não use UUID como senha, token de recuperação ou segredo de autenticação.
Neste guia, você vai escolher a versão correta, modelar um ID de pedido brasileiro, serializar para JSON e banco, validar entradas de API e testar o comportamento sem acoplar a suíte a valores aleatórios frágeis.
O que é um UUID e quando ele resolve o problema
UUID significa Universally Unique Identifier. Na prática, é um valor de 128 bits com probabilidade de colisão desprezível em cenários reais de aplicação. Em texto, o formato canônico tem 36 caracteres:
550e8400-e29b-41d4-a716-446655440000
Há oito grupos hexadecimais separados por hífen. O valor não depende de um contador central: dois serviços, em máquinas diferentes, podem gerar IDs sem coordenar um banco de sequências.
Isso é útil quando:
- vários microsserviços criam recursos em paralelo;
- um worker offline precisa gerar IDs antes de sincronizar;
- você não quer expor um contador sequencial de pedidos (
1,2,3); - logs, filas e traces precisam de um
request_idestável entre sistemas.
Em contrapartida, UUID não é a única opção. Para códigos curtos de uso humano, como cupom PX-7K2M, um gerador próprio pode ser melhor. Para chaves internas de um único banco, BIGSERIAL continua simples e eficiente. O UUID brilha quando a unicidade precisa atravessar processos, serviços e tempo.
Como gerar UUID4, a opção mais comum
A versão 4 é aleatória. Em Python:
import uuid
pedido_id = uuid.uuid4()
print(pedido_id)
print(type(pedido_id))
print(pedido_id.version)
A saída típica:
3f1c2a8e-9c4b-4d7e-8a11-0b5f2c9e1d44
<class 'uuid.UUID'>
4
uuid4() devolve um objeto uuid.UUID, não uma string. Isso é importante: o objeto tem métodos e propriedades úteis e evita que você trate o identificador como texto livre cedo demais.
Converta para string apenas nas bordas do sistema:
import uuid
def novo_pedido_id() -> uuid.UUID:
return uuid.uuid4()
def serializar_id(valor: uuid.UUID) -> str:
return str(valor)
Use o objeto no domínio e a string em JSON, query string, planilha exportada ou mensagem de log.
UUID7: ordem temporal e índices mais amigáveis
UUID4 espalha valores de forma aleatória. Em tabelas grandes, isso pode fragmentar índices B-tree e piorar inserções em massa. A versão 7 embute um carimbo de tempo Unix em milissegundos no início do valor, o que tende a manter as inserções mais ordenadas.
A partir do Python 3.14, a biblioteca padrão oferece:
import uuid
evento_id = uuid.uuid7()
print(evento_id)
print(evento_id.version)
Se o ambiente ainda estiver em uma versão anterior, documente a dependência de geração ou use um pacote maduro até migrar. Em qualquer caso, o contrato da API deve deixar claro qual versão você emite.
Quando preferir UUID7:
- chaves primárias de alto volume de escrita;
- eventos, logs e outbox patterns;
- filas em que a ordem aproximada de criação ajuda a depuração.
Quando UUID4 ainda é suficiente:
- volume moderado;
- necessidade máxima de portabilidade entre linguagens e bibliotecas antigas;
- identificadores públicos em que a ordenação temporal não é desejável.
UUID5: o mesmo input, o mesmo identificador
UUID5 é determinístico. Dado um namespace e um nome, o resultado se repete:
import uuid
NAMESPACE_CLIENTES = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
def id_cliente_externo(codigo_erp: str) -> uuid.UUID:
return uuid.uuid5(NAMESPACE_CLIENTES, codigo_erp.strip().upper())
print(id_cliente_externo("ACME-001"))
print(id_cliente_externo("acme-001"))
As duas chamadas produzem o mesmo valor porque o nome foi normalizado. Isso é útil em integrações:
- mapear um código de ERP legado para um UUID estável;
- deduplicar contatos importados de planilhas;
- gerar IDs reproduzíveis em pipelines de ETL.
Cuidados:
- o namespace precisa ser estável e documentado;
- a normalização do nome (
strip, caixa, acentos) faz parte do contrato; - se a regra de normalização mudar, o ID muda.
Para namespaces próprios, crie um UUID4 uma vez e grave-o no código ou na configuração:
import uuid
# Gerado uma vez e versionado com o projeto
NAMESPACE_PEDIDOS_BR = uuid.UUID("9f3c1a2e-7b54-4d0a-9c21-8e6f0b1a2c3d")
Não recrie o namespace a cada deploy.
UUID1 e por que ele costuma ser evitado em APIs públicas
UUID1 combina tempo e identificador de nó, historicamente o endereço MAC. Em alguns ambientes isso facilita ordenação, mas pode vazar informação de hardware ou de rede.
import uuid
print(uuid.uuid1())
Em APIs expostas na internet, prefira UUID4 ou UUID7. Se um sistema legado já depende de UUID1, isole a geração e não propague o valor como se fosse um segredo.
Objeto UUID, string, bytes e int
O tipo uuid.UUID aceita várias construções:
import uuid
a = uuid.UUID("550e8400-e29b-41d4-a716-446655440000")
b = uuid.UUID(hex="550e8400e29b41d4a716446655440000")
c = uuid.UUID(bytes=a.bytes)
d = uuid.UUID(int=a.int)
assert a == b == c == d
Propriedades úteis:
valor = uuid.uuid4()
print(valor.hex) # 32 caracteres sem hífen
print(valor.urn) # urn:uuid:...
print(valor.bytes) # 16 bytes
print(valor.version) # 4, 5, 7...
print(valor.variant)
Para chaves em cache Redis ou nomes de arquivo, .hex é prático. Para bancos com tipo nativo, o driver costuma aceitar o objeto ou a string canônica. Para armazenamento binário compacto, use 16 bytes.
Validar UUID vindo de API, formulário ou planilha
Nunca confie em texto livre. Valide na borda:
import uuid
from dataclasses import dataclass
class IdInvalidoError(ValueError):
pass
def parse_uuid(texto: str) -> uuid.UUID:
try:
return uuid.UUID(str(texto).strip())
except (ValueError, AttributeError, TypeError) as exc:
raise IdInvalidoError("Identificador inválido.") from exc
@dataclass(frozen=True, slots=True)
class PedidoRef:
id: uuid.UUID
@classmethod
def from_texto(cls, texto: str) -> "PedidoRef":
return cls(id=parse_uuid(texto))
Em FastAPI, o próprio tipo uuid.UUID já faz a validação do path e da query:
from fastapi import FastAPI, HTTPException
import uuid
app = FastAPI()
PEDIDOS: dict[uuid.UUID, dict] = {}
@app.get("/pedidos/{pedido_id}")
def obter_pedido(pedido_id: uuid.UUID) -> dict:
pedido = PEDIDOS.get(pedido_id)
if pedido is None:
raise HTTPException(status_code=404, detail="Pedido não encontrado.")
return pedido
O tutorial de APIs REST com FastAPI mostra como encaixar isso em roteadores, status codes e contratos de resposta.
Modelo prático: pedido com ID público e número legível
Em e-commerce e backoffice brasileiros, é comum precisar de dois identificadores:
- um UUID interno/público estável para integrações;
- um número legível para atendimento, nota e boleto.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from decimal import Decimal
import uuid
def agora_utc() -> datetime:
return datetime.now(timezone.utc)
@dataclass(slots=True)
class Pedido:
cliente: str
total: Decimal
id: uuid.UUID = field(default_factory=uuid.uuid4)
numero: str = field(init=False)
criado_em: datetime = field(default_factory=agora_utc)
def __post_init__(self) -> None:
# Número legível separado do UUID
stamp = self.criado_em.strftime("%Y%m%d")
sufixo = self.id.hex[:6].upper()
self.numero = f"BR-{stamp}-{sufixo}"
pedido = Pedido(cliente="Loja Centro SP", total=Decimal("199.90"))
print(pedido.id)
print(pedido.numero)
O UUID permanece a chave técnica. O numero ajuda suporte e operação. Não tente “encurtar” o UUID para virar código de boleto sem um desenho explícito de colisão e validação.
Para dinheiro e arredondamento do total, use Decimal como no guia de Decimal em Python.
UUID no banco de dados
SQLite
SQLite não tem um tipo nativo UUID. O caminho usual é gravar texto canônico ou blob de 16 bytes:
import sqlite3
import uuid
conn = sqlite3.connect(":memory:")
conn.execute(
"""
CREATE TABLE pedidos (
id TEXT PRIMARY KEY,
cliente TEXT NOT NULL
)
"""
)
pedido_id = uuid.uuid4()
conn.execute(
"INSERT INTO pedidos (id, cliente) VALUES (?, ?)",
(str(pedido_id), "Cliente Demo"),
)
conn.commit()
row = conn.execute(
"SELECT id, cliente FROM pedidos WHERE id = ?",
(str(pedido_id),),
).fetchone()
lido = uuid.UUID(row[0])
assert lido == pedido_id
O artigo de SQLite com Python cobre conexões, parâmetros e organização de scripts.
PostgreSQL e SQLAlchemy
No PostgreSQL, prefira o tipo uuid. Com SQLAlchemy 2:
import uuid
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
class Base(DeclarativeBase):
pass
class Cliente(Base):
__tablename__ = "clientes"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
nome: Mapped[str] = mapped_column(String(120), nullable=False)
as_uuid=True mantém o valor como objeto uuid.UUID no Python. O guia de SQLAlchemy 2 aprofunda mapeamentos, sessões e migrações.
Se a tabela for de alto volume de inserts, avalie UUID7 ou um gerador de ID ordenável. Meça com dados reais antes de otimizar por intuição.
JSON, logs e correlação entre serviços
Em JSON, o UUID viaja como string:
import json
import uuid
payload = {
"pedido_id": str(uuid.uuid4()),
"status": "criado",
}
print(json.dumps(payload, ensure_ascii=False))
Em logs, padronize o nome do campo:
import logging
import uuid
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("api")
request_id = uuid.uuid4()
logger.info("request_started request_id=%s path=/checkout", request_id)
Repasse o mesmo request_id em cabeçalhos como X-Request-ID para correlacionar API, worker e banco. O tutorial de logging em Python mostra formatação, handlers e boas práticas de observabilidade.
Evite logar juntos UUID e dados sensíveis sem necessidade. Identificador de pedido é uma coisa; token de sessão é outra.
UUID não substitui secrets
Essa confusão aparece com frequência em code review:
| Necessidade | Ferramenta |
|---|---|
| Identificar um pedido publicamente | uuid |
| Correlacionar logs entre serviços | uuid |
| Link de recuperação de senha | secrets |
| Chave de API com privilégio | secrets + hash no banco |
| Senha de usuário | Argon2id / scrypt / bcrypt |
Um UUID4 tem muita entropia, mas o ecossistema e o ciclo de vida são diferentes. Tokens de segurança pedem expiração, uso único, comparação em tempo constante e armazenamento com hash. Isso está no guia de secrets em Python.
Regra simples: se o valor identifica, pense em uuid. Se o valor autoriza, pense em secrets.
Versionar, comparar e ordenar
Objetos UUID são comparáveis:
import uuid
a = uuid.UUID("00000000-0000-4000-8000-000000000001")
b = uuid.UUID("00000000-0000-4000-8000-000000000002")
print(a < b)
print({a, a, b})
A comparação usa o valor inteiro de 128 bits. Com UUID4, a ordem não representa tempo de criação. Com UUID7, a ordem costuma acompanhar o instante de geração, o que ajuda em listagens e varreduras.
Para deduplicar:
import uuid
vistos: set[uuid.UUID] = set()
for bruto in ["550e8400-e29b-41d4-a716-446655440000", "550e8400e29b41d4a716446655440000"]:
vistos.add(uuid.UUID(bruto))
print(len(vistos)) # 1
A construção via uuid.UUID(...) normaliza hífen e caixa.
Testes sem flakiness
Não faça assert de um UUID4 “mágico” gerado dentro da função sem injeção. Prefira uma destas estratégias.
1. Injetar a fábrica
import uuid
from collections.abc import Callable
def criar_pedido(
cliente: str,
novo_id: Callable[[], uuid.UUID] | None = None,
) -> dict:
gerar = novo_id or uuid.uuid4
return {"id": gerar(), "cliente": cliente}
def test_criar_pedido_usa_id_fornecido() -> None:
fixo = uuid.UUID("12345678-1234-4678-9234-567812345678")
pedido = criar_pedido("Ana", novo_id=lambda: fixo)
assert pedido["id"] == fixo
2. Validar formato e versão
import uuid
def test_uuid4_tem_versao_correta() -> None:
valor = uuid.uuid4()
assert isinstance(valor, uuid.UUID)
assert valor.version == 4
assert str(valor).count("-") == 4
3. Monkeypatch pontual
import uuid
def test_com_monkeypatch(monkeypatch) -> None:
fixo = uuid.UUID("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee")
monkeypatch.setattr(uuid, "uuid4", lambda: fixo)
assert uuid.uuid4() == fixo
O guia de testes unitários em Python aprofunda fixtures, parametrização e isolamento.
Erros comuns e como evitar
- Usar
str(uuid.uuid4())cedo demais no domínio. Prefira o objeto e serialize nas bordas. - Aceitar qualquer string sem
uuid.UUID(...). Entradas com espaços, aspas ou truncamento passam despercebidas. - Trocar UUID por contador sequencial exposto. Facilita enumeração de pedidos e usuários.
- Gerar UUID1 em API pública sem avaliar o impacto. Pode vazar metadados indesejados.
- Usar UUID como senha ou token de reset. O ciclo de vida de segurança é outro.
- Mudar a normalização de UUID5 sem migração. IDs históricos deixam de bater.
- Comparar strings com caixas e hífens diferentes. Normalize com o construtor
UUID. - Assumir que UUID4 ordena por tempo. Não ordena; use UUID7 ou um campo
created_at. - Colocar UUID em nomes de arquivo sem sanitizar o restante do caminho. O ID é seguro, o prefixo do usuário pode não ser. Veja pathlib.
- Ignorar o tipo no ORM. Gravar como
VARCHAR(36)sem índice ou como texto inconsistente gera dor em joins e uniques.
Checklist para adotar UUID no projeto
- O identificador é de recurso, não de autenticação;
- a versão (4, 5 ou 7) está documentada;
- a validação acontece na borda da API;
- o domínio trabalha com
uuid.UUID; - JSON e logs usam string canônica;
- o banco tem tipo e índice adequados;
- UUID5 tem namespace estável e regra de normalização;
- testes não dependem de valores aleatórios opacos;
- há um número legível separado quando o negócio precisa;
- tokens e segredos usam
secrets, nãouuid.
Perguntas frequentes
Qual UUID devo usar em uma API nova em Python?
Para a maioria dos casos, UUID4 ou UUID7. UUID4 é aleatório, simples e universalmente suportado. UUID7 melhora a localidade temporal e costuma se comportar melhor em índices de escrita intensa. Escolha UUID5 apenas quando a estabilidade determinística for um requisito explícito de integração.
UUID é a mesma coisa que um token de segurança?
Não. UUID identifica. Token autoriza. Um pedido_id público pode ser UUID. Um link de redefinição de senha deve nascer de secrets, com expiração, uso único e hash no armazenamento. Misturar os dois conceitos é uma fonte clássica de falha de modelagem.
Posso usar UUID como chave primária no PostgreSQL?
Sim. O PostgreSQL oferece tipo nativo e o ecossistema Python o suporta bem. Em tabelas muito grandes, meça o impacto do UUID4 no índice e considere UUID7 ou outro identificador ordenável. Em SQLite, texto canônico ou blob de 16 bytes são os caminhos usuais.
Qual a diferença entre UUID4 e UUID5?
UUID4 é aleatório: cada chamada gera um valor novo. UUID5 é determinístico: namespace + nome normalizado produzem sempre o mesmo resultado. UUID5 é ideal para mapear códigos legados; UUID4/7 são ideais para novos recursos criados pela aplicação.
O módulo uuid precisa ser instalado com pip?
Não. Ele é biblioteca padrão. Em Python recente você também encontra uuid7. Só busque pacotes externos se precisar de geração avançada em versões antigas ou de utilitários específicos de serialização.
Conclusão
uuid resolve um problema central de sistemas reais: criar identificadores únicos sem depender de um contador global. Com uuid4() você cobre a maior parte das APIs; com uuid7() melhora ordenação e índices; com uuid5() estabiliza integrações determinísticas.
O desenho completo importa mais do que a chamada isolada. Valide na borda, preserve o objeto no domínio, serialize de forma canônica, escolha o tipo certo no banco e separe identificação de autenticação. Esse cuidado aparece em backends, automações, dados e nas vagas de Python do mercado brasileiro: quem modela IDs com clareza evita classes inteiras de bugs de integração e suporte.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português