NewType no Python: IDs sem misturar

Publicado em: 29/08/2026
Tempo de leitura: 6 minutos
Laptop displaying code editor on a desk with a coffee mug beside it, suggesting a workspace or home office setting.

Em muitos sistemas, valores diferentes compartilham a mesma representação em runtime. Um ID de usuário e um ID de pedido podem ser inteiros; um e-mail e um código de país podem ser strings. Mesmo assim, trocar esses valores acidentalmente é um erro de domínio. typing.NewType cria tipos estáticos distintos sobre uma representação existente, sem exigir uma classe completa para cada conceito.

Neste guia, você aprenderá a criar IDs semânticos, validar valores em funções de construção, trabalhar com bancos de dados, JSON, dataclasses e APIs, comparar NewType com aliases, subclasses e classes de valor, e reconhecer seus limites em runtime.

O problema dos tipos primitivos

def carregar_usuario(usuario_id: int) -> str:
    ...

def carregar_pedido(pedido_id: int) -> str:
    ...

usuario_id = 10
pedido_id = 20

carregar_usuario(pedido_id)  # passa no analisador

Os dois parâmetros são inteiros, então o verificador não consegue distinguir seus significados.

Criando tipos com NewType

from typing import NewType

UsuarioId = NewType("UsuarioId", int)
PedidoId = NewType("PedidoId", int)

def carregar_usuario(usuario_id: UsuarioId) -> str:
    ...

def carregar_pedido(pedido_id: PedidoId) -> str:
    ...

Agora, passar PedidoId onde UsuarioId é esperado gera um erro estático.

usuario_id = UsuarioId(10)
pedido_id = PedidoId(20)

carregar_usuario(usuario_id)  # correto
carregar_usuario(pedido_id)   # erro de tipagem

Comportamento em runtime

NewType não cria um wrapper de dados tradicional. Chamar UsuarioId(10) devolve o próprio inteiro em runtime.

valor = UsuarioId(10)
print(valor)          # 10
print(type(valor))    # int

A distinção existe principalmente para o analisador. Isso mantém interoperabilidade e baixo custo, mas significa que NewType não impõe validação sozinho.

NewType não é uma classe para isinstance

isinstance(valor, UsuarioId)  # inadequado

O objeto criado por NewType não deve ser usado como classe de runtime em isinstance() ou issubclass(). Para verificações reais, teste o tipo base ou use uma classe de valor.

NewType versus alias

UsuarioId = int

Isso é apenas um alias. UsuarioId e int são o mesmo tipo para o analisador. Com NewType, UsuarioId é distinto de int na direção de entrada: uma função que exige UsuarioId não deve aceitar um int arbitrário sem construção explícita.

Relação com o tipo base

Um valor NewType pode ser usado onde o tipo base é aceito.

def duplicar(valor: int) -> int:
    return valor * 2

usuario_id = UsuarioId(10)
resultado = duplicar(usuario_id)

UsuarioId é tratado como subtipo estático de int. A relação inversa não é automática: um int comum não é UsuarioId.

Construção não significa validação

usuario_id = UsuarioId(-10)

NewType não verifica se o número é positivo. A chamada apenas marca o valor. Para invariantes, crie uma função de parsing ou construção.

def criar_usuario_id(valor: int) -> UsuarioId:
    if valor <= 0:
        raise ValueError("ID deve ser positivo")
    return UsuarioId(valor)

Centralizar a construção reduz marcações incorretas espalhadas pelo código.

Parsing de strings

def parse_usuario_id(texto: str) -> UsuarioId:
    try:
        valor = int(texto)
    except ValueError as erro:
        raise ValueError("ID inválido") from erro
    return criar_usuario_id(valor)

O parser converte e valida dados externos antes de devolver o tipo semântico.

IDs em dataclasses

from dataclasses import dataclass

@dataclass(frozen=True)
class Usuario:
    id: UsuarioId
    nome: str

@dataclass(frozen=True)
class Pedido:
    id: PedidoId
    usuario_id: UsuarioId

Os campos documentam o domínio e impedem associações acidentais durante construção e refatoração.

Repositórios tipados

class RepositorioUsuarios:
    def obter(self, id_: UsuarioId) -> Usuario | None:
        ...

    def excluir(self, id_: UsuarioId) -> None:
        ...

O contrato deixa claro qual identificador cada repositório aceita.

Banco de dados

Drivers de banco devolvem tipos básicos. A camada de persistência deve converter explicitamente.

def usuario_da_linha(linha: tuple[int, str]) -> Usuario:
    id_bruto, nome = linha
    return Usuario(id=UsuarioId(id_bruto), nome=nome)

Se o banco não garante a invariante, use a função validada criar_usuario_id().

JSON e APIs

Serialização geralmente usa o tipo base.

def usuario_para_json(usuario: Usuario) -> dict[str, object]:
    return {
        "id": int(usuario.id),
        "nome": usuario.nome,
    }

Como o valor já é um int em runtime, a conversão pode ser opcional, mas torná-la explícita melhora a leitura na fronteira.

NewType com strings

Email = NewType("Email", str)
CodigoPais = NewType("CodigoPais", str)

def enviar(email: Email, mensagem: str) -> None:
    ...

Uma função de construção pode normalizar e validar o e-mail antes de retornar Email.

def criar_email(valor: str) -> Email:
    normalizado = valor.strip().casefold()
    if "@" not in normalizado:
        raise ValueError("e-mail inválido")
    return Email(normalizado)

Valores financeiros

NewType pode separar centavos, pontos ou quantidades, mas não adiciona operações seguras, arredondamento ou moeda.

Centavos = NewType("Centavos", int)
Pontos = NewType("Pontos", int)

Para dinheiro com regras complexas, uma dataclass ou classe de valor pode ser melhor. NewType é adequado quando a representação básica já possui todas as operações necessárias e a principal necessidade é evitar mistura.

NewType e coleções

usuarios: list[UsuarioId] = [UsuarioId(1), UsuarioId(2)]
pedidos: list[PedidoId] = [PedidoId(10)]

O analisador impede combinar coleções semanticamente diferentes, embora ambas contenham inteiros em runtime.

Dicionários e mapas

nomes: dict[UsuarioId, str] = {
    UsuarioId(1): "Ana",
}

pedido_por_usuario: dict[UsuarioId, list[PedidoId]] = {}

As chaves e valores mostram as relações do domínio de forma mais precisa.

Retornos de funções

def criar_usuario(nome: str) -> UsuarioId:
    id_bruto = inserir_no_banco(nome)
    return UsuarioId(id_bruto)

O chamador recebe um identificador já classificado e não precisa adivinhar o significado do inteiro.

NewType e Optional

def encontrar_usuario(email: Email) -> UsuarioId | None:
    ...

A união mantém a distinção semântica. Depois de tratar None, o valor restante é UsuarioId.

NewType aninhado

É possível criar um NewType baseado em outro, mas camadas excessivas podem confundir.

Id = NewType("Id", int)
UsuarioId = NewType("UsuarioId", Id)

Use essa hierarquia somente se a relação de subtipo for útil. Na maioria dos projetos, NewTypes diretamente baseados em int ou str são mais simples.

NewType versus subclasse de int

class UsuarioIdRuntime(int):
    pass

Uma subclasse real existe em runtime e pode ser usada com isinstance(). Porém, construção, serialização, operadores e resultados de operações podem exigir cuidado. NewType é mais leve quando a distinção é apenas estática.

NewType versus dataclass de valor

@dataclass(frozen=True)
class UsuarioIdValor:
    valor: int

    def __post_init__(self) -> None:
        if self.valor <= 0:
            raise ValueError("ID inválido")

A dataclass impõe invariantes, possui identidade de runtime e pode oferecer métodos, mas exige acesso a .valor e conversões. Escolha conforme a necessidade de comportamento.

NewType versus Annotated

Annotated adiciona metadados a um tipo, mas normalmente não torna duas anotações distintas para o analisador.

from typing import Annotated

UsuarioIdDoc = Annotated[int, "usuario"]

Use NewType para distinção estática. Use Annotated quando frameworks, validadores ou documentação precisam de metadados.

NewType em bibliotecas públicas

Exportar NewTypes melhora contratos, mas pode ser uma mudança incompatível para usuários que passavam primitivos diretamente. Planeje migrações, ofereça factories e documente fronteiras onde a construção é esperada.

Operações aritméticas

usuario_id = UsuarioId(10)
proximo = usuario_id + 1

O resultado de uma operação do tipo base costuma ser int, não UsuarioId. Isso é desejável: somar um identificador pode não produzir automaticamente outro identificador válido. Construa novamente apenas quando a operação fizer sentido no domínio.

Evite marcar valores arbitrários

def funcao_perigosa(valor: int) -> UsuarioId:
    return UsuarioId(valor)

Se qualquer parte do sistema puder converter qualquer int sem validação, a proteção perde valor. Restrinja a construção a parsers, repositórios e factories confiáveis.

Erros comuns

  • Usar alias em vez de NewType: não cria distinção estática.
  • Esperar validação automática: NewType devolve o valor base.
  • Usar isinstance com NewType: ele não é uma classe de domínio em runtime.
  • Marcar dados externos sem validar: a anotação não prova a invariante.
  • Esperar que operações preservem o NewType: resultados normalmente voltam ao tipo base.
  • Criar dezenas de tipos sem benefício: use onde erros de mistura são plausíveis e relevantes.

Exemplo completo: serviço de transferências

from dataclasses import dataclass
from typing import NewType

ContaId = NewType("ContaId", int)
Centavos = NewType("Centavos", int)

@dataclass(frozen=True)
class Transferencia:
    origem: ContaId
    destino: ContaId
    valor: Centavos

def criar_conta_id(valor: int) -> ContaId:
    if valor <= 0:
        raise ValueError("conta inválida")
    return ContaId(valor)

def criar_centavos(valor: int) -> Centavos:
    if valor <= 0:
        raise ValueError("valor deve ser positivo")
    return Centavos(valor)

def transferir(
    origem: ContaId,
    destino: ContaId,
    valor: Centavos,
) -> Transferencia:
    if origem == destino:
        raise ValueError("contas devem ser diferentes")
    registrar_transferencia(origem, destino, valor)
    return Transferencia(origem, destino, valor)

O analisador impede passar Centavos como ContaId ou misturar identificadores com valores. As factories cuidam das regras de runtime.

Testando a tipagem

Inclua testes estáticos com chamadas inválidas esperadas. Use reveal_type() para confirmar resultados de operações. Testes de runtime devem focar nas factories, pois é nelas que as invariantes são realmente verificadas.

Conclusão

typing.NewType cria distinções semânticas leves sobre int, str e outros tipos existentes. Ele é ideal para IDs, códigos, unidades e valores que compartilham representação, mas não devem ser misturados.

A documentação oficial de NewType no Python detalha a semântica. Combine NewType com factories validadas nas fronteiras e escolha classes de valor quando precisar de comportamento, invariantes fortes ou identidade real em runtime.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Close-up view of a computer screen displaying code in a software development environment.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Never no Python: marque código inalcançável

    Aprenda typing.Never no Python para funções que não retornam, código inalcançável e verificação exaustiva com assert_never.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Macro shot capturing detailed patterns of a python in its natural surroundings.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Concatenate no Python: altere parâmetros

    Aprenda Concatenate no Python para adicionar ou ocultar parâmetros em decoradores tipados com ParamSpec, contexto e dependências.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A close-up view of a person's hand signing a business contract on a desk with a pen.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    ParamSpec no Python: preserve assinaturas

    Aprenda ParamSpec no Python para preservar assinaturas em decoradores, callbacks, wrappers async e funções de ordem superior.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TypeIs no Python: refine os dois ramos

    Aprenda TypeIs no Python para refinar tipos nos ramos verdadeiro e falso, comparar com TypeGuard e criar predicados seguros.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TypeGuard no Python: refine tipos com segurança

    Aprenda TypeGuard no Python para refinar tipos, validar coleções, TypedDict e Protocol com segurança estática e checagem real.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    typing.Self no Python: retornos fluentes

    Aprenda typing.Self no Python para métodos fluentes, classmethods, builders, clones, Protocol, context managers e retornos que preservam subclasses.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026