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.

11 min de leitura Equipe Python Brasil

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_id está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:

  1. um UUID interno/público estável para integrações;
  2. 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:

NecessidadeFerramenta
Identificar um pedido publicamenteuuid
Correlacionar logs entre serviçosuuid
Link de recuperação de senhasecrets
Chave de API com privilégiosecrets + hash no banco
Senha de usuárioArgon2id / 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

  1. Usar str(uuid.uuid4()) cedo demais no domínio. Prefira o objeto e serialize nas bordas.
  2. Aceitar qualquer string sem uuid.UUID(...). Entradas com espaços, aspas ou truncamento passam despercebidas.
  3. Trocar UUID por contador sequencial exposto. Facilita enumeração de pedidos e usuários.
  4. Gerar UUID1 em API pública sem avaliar o impacto. Pode vazar metadados indesejados.
  5. Usar UUID como senha ou token de reset. O ciclo de vida de segurança é outro.
  6. Mudar a normalização de UUID5 sem migração. IDs históricos deixam de bater.
  7. Comparar strings com caixas e hífens diferentes. Normalize com o construtor UUID.
  8. Assumir que UUID4 ordena por tempo. Não ordena; use UUID7 ou um campo created_at.
  9. 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.
  10. 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ão uuid.

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.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português