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

    Estructura de código y XML para canonicalización con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ElementTree canonicalize: normaliza XML en Python

    Aprende ElementTree canonicalize en Python para comparar XML, generar hashes estables y controlar namespaces, espacios y comentarios.

    Ler mais

    Tempo de leitura: 5 minutos
    22/10/2026
    Código Python en una pantalla
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    T-strings y templatelib: interpolación estructurada

    Aprende t-strings y string.templatelib en Python para interpolación estructurada, renderizado seguro, logs y procesamiento personalizado.

    Ler mais

    Tempo de leitura: 6 minutos
    22/10/2026
    Base SQLite y código Python para exportación selectiva con iterdump
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    SQLite iterdump: crea dumps selectivos en Python

    Aprende sqlite3.Connection.iterdump con filtro para crear dumps SQLite selectivos, validar dependencias y restaurar con seguridad.

    Ler mais

    Tempo de leitura: 4 minutos
    21/10/2026
    Código y archivos que representan compresión de datos con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Zstandard rápido en Python

    Aprende compression.zstd en Python para comprimir archivos y datos con velocidad, streaming, integridad y seguridad.

    Ler mais

    Tempo de leitura: 7 minutos
    21/10/2026
    Archivo de configuración y estructura de código para ConfigParser en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ConfigParser: lee INI sin sección en Python

    Aprende ConfigParser sin sección en Python con validación, tipos, compatibilidad, capas de configuración y escritura segura.

    Ler mais

    Tempo de leitura: 5 minutos
    20/10/2026
    Datos binarios y memoria de ordenador representando memoryview en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    memoryview count e index: busca sin copiar

    Aprende memoryview count e index en Python para buscar valores en buffers sin copias y validar datos binarios con eficiencia.

    Ler mais

    Tempo de leitura: 5 minutos
    20/10/2026