NewType en Python: separa identificadores

Publicado el: 29/08/2026
Tempo de leitura: 5 minutos
Detailed view of programming code in a dark theme on a computer screen.

Muchos sistemas usan la misma representación de runtime para conceptos distintos. Un ID de usuario y un ID de pedido pueden ser enteros; un correo y un código de país pueden ser strings. Pasar uno donde se espera el otro sigue siendo un error de dominio. typing.NewType crea tipos estáticos distintos sobre una representación existente sin exigir una clase completa para cada concepto.

Esta guía cubre IDs semánticos, construcción validada, bases de datos, JSON, dataclasses, APIs, aliases, subclases, objetos de valor y límites de NewType en runtime.

El problema de los tipos primitivos

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

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

usuario_id = 10
pedido_id = 20
cargar_usuario(pedido_id)  # aceptado

Ambos parámetros son enteros y el analizador no distingue su significado.

Crear tipos semánticos

from typing import NewType

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

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

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

Pasar PedidoId donde se espera UsuarioId ahora produce un error estático.

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

cargar_usuario(usuario_id)
cargar_usuario(pedido_id)  # error de tipado

Comportamiento en runtime

NewType no crea un wrapper de datos tradicional. Llamar UsuarioId(10) devuelve el entero base en runtime.

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

La distinción existe principalmente para el análisis estático. Esto mantiene interoperabilidad y bajo coste, pero no impone validación.

NewType no es una clase de runtime

isinstance(valor, UsuarioId)  # no apropiado

El objeto NewType no es una clase de dominio para usar con isinstance() o issubclass(). Comprueba el tipo base o usa una clase de valor real cuando la identidad de runtime sea necesaria.

NewType frente a alias

UsuarioId = int

Esto es solo un alias. UsuarioId e int siguen siendo idénticos para el analizador. Con NewType, UsuarioId es distinto de un int arbitrario al entrar en una función.

Relación con el tipo base

Un valor NewType puede usarse donde se acepta el tipo base.

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

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

UsuarioId actúa como subtipo estático de int. La dirección inversa no es automática: un int común no es UsuarioId hasta construirlo explícitamente.

Construcción no es validación

usuario_id = UsuarioId(-10)

NewType no comprueba que el número sea positivo. Coloca las invariantes en una factory o parser.

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

La construcción centralizada evita marcas arbitrarias por todo el código.

Parsing de strings

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

El parser convierte y valida datos externos antes de devolver el tipo semántico.

IDs en dataclasses

from dataclasses import dataclass

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

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

Los campos documentan el dominio y evitan asociaciones accidentales durante construcción y refactorización.

Repositorios tipados

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

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

El contrato identifica claramente qué clase de ID acepta el repositorio.

Frontera de base de datos

Los drivers devuelven tipos base. La capa de persistencia debe clasificarlos.

def usuario_desde_fila(fila: tuple[int, str]) -> Usuario:
    id_bruto, nombre = fila
    return Usuario(id=UsuarioId(id_bruto), nombre=nombre)

Si la base no garantiza la invariante, usa la factory validada.

JSON y APIs

La serialización normalmente expone la representación base.

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

La conversión puede ser opcional en runtime, pero hacerla explícita documenta la frontera.

NewTypes de string

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

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

Una factory puede normalizar y validar.

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

Valores financieros y unidades

NewType puede separar centavos, puntos y cantidades, pero no añade redondeo, reglas de moneda u operaciones seguras.

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

Para dinero con reglas complejas, una clase de valor puede ser mejor. NewType funciona cuando las operaciones base son suficientes y la necesidad principal es evitar mezclas.

Colecciones

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

El analizador mantiene separadas colecciones semánticamente diferentes aunque contengan enteros en runtime.

Diccionarios

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

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

Las anotaciones muestran las relaciones del dominio.

Retornos de funciones

def crear_usuario(nombre: str) -> UsuarioId:
    id_bruto = insertar_en_base(nombre)
    return UsuarioId(id_bruto)

El consumidor recibe un identificador ya clasificado y no necesita adivinar el significado del entero.

Optional

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

Después de tratar None, el valor restante sigue siendo UsuarioId.

NewType anidado

Un NewType puede basarse en otro, pero demasiadas capas confunden.

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

Usa esta jerarquía solo si la relación aporta valor. Bases directas int o str suelen ser más simples.

NewType frente a subclase de int

class UsuarioIdRuntime(int):
    pass

Una subclase real existe en runtime y admite isinstance(). Construcción, serialización y resultados de operadores pueden requerir atención. NewType es más ligero cuando la distinción es solo estática.

NewType frente a dataclass de valor

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

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

La dataclass impone invariantes, tiene identidad de runtime y puede ofrecer métodos. También requiere acceder a .valor y convertir explícitamente.

NewType frente a Annotated

Annotated adjunta metadatos, pero normalmente no crea distinción para el analizador.

from typing import Annotated

UsuarioIdDocumentado = Annotated[int, "usuario"]

Usa NewType para distinción estática y Annotated para metadatos consumidos por frameworks, validadores o documentación.

Diseño de bibliotecas públicas

Exportar NewTypes mejora contratos, pero puede ser un cambio incompatible para consumidores que pasaban primitivos. Planifica migraciones, ofrece factories y documenta dónde se espera construcción explícita.

Resultados de operaciones

usuario_id = UsuarioId(10)
siguiente = usuario_id + 1

El resultado suele ser int, no UsuarioId. Esto es razonable: sumar a un identificador no produce automáticamente otro ID válido.

Evitar marcas arbitrarias

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

Si cualquier capa puede marcar enteros sin validar, la protección pierde significado. Mantén la construcción en parsers, repositorios y factories confiables.

Errores comunes

  • Usar un alias: no crea distinción estática.
  • Esperar validación automática: NewType devuelve el valor base.
  • Usar isinstance con NewType: no es una clase de dominio en runtime.
  • Marcar datos externos sin comprobar: la anotación no prueba invariantes.
  • Esperar que operaciones preserven NewType: suelen volver al tipo base.
  • Crear demasiados tipos: enfócate en mezclas plausibles e importantes.

Ejemplo completo: transferencias

from dataclasses import dataclass
from typing import NewType

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

@dataclass(frozen=True)
class Transferencia:
    origen: CuentaId
    destino: CuentaId
    valor: Centavos

def crear_cuenta_id(valor: int) -> CuentaId:
    if valor <= 0:
        raise ValueError("cuenta inválida")
    return CuentaId(valor)

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

def transferir(
    origen: CuentaId,
    destino: CuentaId,
    valor: Centavos,
) -> Transferencia:
    if origen == destino:
        raise ValueError("las cuentas deben ser distintas")
    registrar_transferencia(origen, destino, valor)
    return Transferencia(origen, destino, valor)

El analizador impide pasar Centavos como CuentaId. Las factories imponen reglas de runtime.

Probar el tipado

Incluye pruebas estáticas con llamadas inválidas y usa reveal_type() para inspeccionar resultados de operaciones. Las pruebas de runtime deben centrarse en las factories.

Conclusión

typing.NewType crea distinciones semánticas ligeras sobre int, str y otros tipos. Es ideal para IDs, códigos, unidades y valores que comparten representación pero no deben mezclarse.

La documentación oficial de NewType en Python define su semántica. Combínalo con factories validadas y elige clases de valor cuando necesites comportamiento, invariantes fuertes o identidad real en runtime.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Annotated en Python: tipos con metadatos

    Aprende Annotated en Python para añadir metadatos a tipos, crear validación, schemas, unidades e integraciones con frameworks.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Vivid close-up of code on a computer screen showcasing programming details.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Never en Python: marca código inalcanzable

    Aprende typing.Never en Python para funciones sin retorno, código inalcanzable y exhaustividad con assert_never, Literal y Enum.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Concatenate en Python: cambia parámetros

    Aprende Concatenate en Python para añadir u ocultar parámetros iniciales en decoradores tipados con ParamSpec y dependencias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ParamSpec en Python: conserva firmas

    Aprende ParamSpec en Python para conservar firmas completas en decoradores, callbacks, wrappers async y funciones de orden superior.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up of a python snake curled up, showcasing its detailed scales.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypeIs en Python: refina ambas ramas

    Aprende TypeIs en Python para refinar las ramas verdadera y falsa, compararlo con TypeGuard y crear predicados de tipo seguros.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypeGuard en Python: refina tipos seguros

    Aprende TypeGuard en Python para refinar tipos y validar colecciones, TypedDict, Protocol y datos externos con comprobaciones reales.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026