---
title: "uuid em Python: identificadores únicos para APIs, banco e logs"
url: "https://python.dev.br/blog/python-uuid-identificadores-unicos/"
markdown_url: "https://python.dev.br/blog/python-uuid-identificadores-unicos.MD"
description: "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."
date: "2026-07-28"
author: "Equipe Python Brasil"
---

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

```text
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:

```python
import uuid

pedido_id = uuid.uuid4()
print(pedido_id)
print(type(pedido_id))
print(pedido_id.version)
```

A saída típica:

```text
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:

```python
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:

```python
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:

```python
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:

```python
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.

```python
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:

```python
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:

```python
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:

```python
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:

```python
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](/blog/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.

```python
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](/blog/python-decimal-dinheiro-calculos-precisos/).

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

```python
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](/blog/python-e-banco-de-dados-sqlite/) cobre conexões, parâmetros e organização de scripts.

### PostgreSQL e SQLAlchemy

No PostgreSQL, prefira o tipo `uuid`. Com SQLAlchemy 2:

```python
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](/blog/sqlalchemy-2-orm-moderno-python/) 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:

```python
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:

```python
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](/blog/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](/blog/python-secrets-tokens-senhas-seguros/).

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:

```python
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:

```python
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

```python
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

```python
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

```python
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](/blog/testes-unitarios-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](/blog/python-pathlib-manipulacao-caminhos-arquivos/).
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](/vagas/) do mercado brasileiro: quem modela IDs com clareza evita classes inteiras de bugs de integração e suporte.
