Annotated en Python: tipos con metadatos

Publicado el: 29/08/2026
Tempo de leitura: 6 minutos
A person typing on a laptop with a Python programming book visible, capturing technology and learning.

Las anotaciones de tipo suelen describir qué valores acepta o devuelve una función. Las aplicaciones reales también necesitan restricciones, unidades, formatos, documentación, reglas de validación e información consumida por frameworks. typing.Annotated permite adjuntar metadatos a un tipo sin cambiar su identidad básica para los analizadores que no entienden esos metadatos.

Esta guía explica tipos enriquecidos, inspección en runtime, dataclasses, APIs, validación, unidades, NewType, integración con frameworks y los límites del diseño basado en metadatos.

Por qué los tipos simples no siempre bastan

def crear_usuario(nombre: str, edad: int) -> None:
    ...

La firma indica que edad es un entero, pero no que deba estar entre 0 y 130. Esa regla puede vivir en documentación, un schema separado o código de validación distante.

Annotated mantiene la información junto al tipo:

from typing import Annotated

Edad = Annotated[int, "0..130"]

def crear_usuario(nombre: str, edad: Edad) -> None:
    ...

Un analizador que ignora metadatos sigue tratando Edad como int. Una herramienta especializada puede interpretar la cadena y aplicar una regla.

Sintaxis básica

Annotated[TipoBase, metadato1, metadato2, ...]

El primer argumento es el tipo real. Los demás pueden ser strings, objetos, enums, dataclasses o cualquier valor entendido por el consumidor.

from dataclasses import dataclass
from typing import Annotated

@dataclass(frozen=True)
class Intervalo:
    minimo: int
    maximo: int

Porcentaje = Annotated[int, Intervalo(0, 100)]

Los objetos estructurados suelen ser más seguros que cadenas libres porque reducen errores de escritura y facilitan la inspección.

Annotated no valida por sí solo

valor: Porcentaje = 500

Python no ejecuta automáticamente Intervalo. La anotación solo transporta metadatos. Un framework, decorador o función debe leerlos y aplicar la regla.

Annotated no sustituye comprobaciones de runtime ni convierte un int en una clase validada.

Cómo lo tratan los analizadores

Las herramientas que no entienden los metadatos deben tratar Annotated[int, ...] como int. Así se mantiene la compatibilidad y varias bibliotecas pueden añadir información propia sin cambiar las relaciones centrales.

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

porcentaje: Porcentaje = 20
resultado = duplicar(porcentaje)

Leer metadatos con get_type_hints

from typing import get_type_hints

def configurar(timeout: Annotated[int, Intervalo(1, 60)]) -> None:
    ...

hints = get_type_hints(configurar, include_extras=True)
print(hints["timeout"])

include_extras=True conserva Annotated y otros detalles. Sin ese argumento, normalmente se obtiene solo el tipo base.

Inspeccionar origen y argumentos

from typing import get_args, get_origin, Annotated

anotacion = get_type_hints(
    configurar,
    include_extras=True,
)["timeout"]

print(get_origin(anotacion))
print(get_args(anotacion))

get_args() devuelve el tipo base seguido de los metadatos. Evita depender de atributos internos privados.

Un validador sencillo

from typing import get_args, get_origin, get_type_hints


def validar_llamada(funcion, argumentos: dict[str, object]) -> None:
    hints = get_type_hints(funcion, include_extras=True)
    for nombre, valor in argumentos.items():
        anotacion = hints.get(nombre)
        if get_origin(anotacion) is Annotated:
            tipo_base, *metadatos = get_args(anotacion)
            if not isinstance(valor, tipo_base):
                raise TypeError(f"{nombre} debe ser {tipo_base}")
            for item in metadatos:
                if isinstance(item, Intervalo):
                    if not item.minimo <= valor <= item.maximo:
                        raise ValueError(f"{nombre} fuera del intervalo")

Es un ejemplo educativo. Los validadores reales deben manejar uniones, genéricos, referencias futuras, subclases, bool frente a int, estructuras anidadas y errores detallados.

Varios objetos de metadatos

@dataclass(frozen=True)
class Descripcion:
    texto: str

@dataclass(frozen=True)
class Unidad:
    nombre: str

Temperatura = Annotated[
    float,
    Unidad("celsius"),
    Intervalo(-273, 1000),
    Descripcion("Temperatura medida por el sensor"),
]

Diferentes herramientas pueden consumir partes distintas. Un generador de documentación lee Descripcion, un validador usa Intervalo y una capa visual interpreta Unidad.

Orden de los metadatos

El orden se conserva y puede importar para la biblioteca. No dependas de reglas implícitas sin documentarlas. Cuando no existe un pipeline, busca metadatos por su tipo y no por posición.

Annotated anidado

Base = Annotated[int, "base"]
Especial = Annotated[Base, "especial"]

Las herramientas pueden aplanar metadatos anidados según reglas definidas. En APIs públicas, prefiere una sola anotación claramente compuesta y prueba la inspección en todas las versiones soportadas.

Aliases reutilizables

IdPositivo = Annotated[int, Intervalo(1, 2_147_483_647)]
NombreCorto = Annotated[str, "1..80 caracteres"]

Los aliases reducen repetición y centralizan convenciones. Cambiar uno compartido afecta muchas APIs, así que trátalo como parte del contrato público.

Annotated y NewType

NewType crea distinción estática; Annotated añade metadatos. Pueden combinarse.

from typing import NewType

UsuarioId = NewType("UsuarioId", int)
UsuarioIdValidado = Annotated[UsuarioId, Intervalo(1, 2_147_483_647)]

El analizador conserva la identidad de UsuarioId y una herramienta de runtime puede comprobar el intervalo. Consulta la guía de NewType en Python.

Annotated y Literal

from typing import Literal

Formato = Annotated[
    Literal["json", "csv"],
    Descripcion("Formato de exportación"),
]

Literal restringe valores exactos; Annotated agrega documentación o comportamiento para herramientas.

Annotated en dataclasses

from dataclasses import dataclass

@dataclass
class Producto:
    nombre: Annotated[str, Descripcion("Nombre público")]
    precio: Annotated[float, Intervalo(0, 1_000_000)]

La dataclass no aplica metadatos automáticamente. Una biblioteca puede inspeccionar las anotaciones y generar validación o schemas.

Annotated en APIs web

Los frameworks pueden usar metadatos para indicar origen de parámetros, límites, ejemplos y documentación, manteniendo un tipo normal para análisis estático.

# Ejemplo conceptual; Query pertenece al framework
Limite = Annotated[int, Query(minimo=1, maximo=100)]

Sigue la documentación oficial del framework. Los objetos no son comprendidos universalmente.

Metadatos como protocolo de biblioteca

Los objetos dentro de Annotated forman un pequeño lenguaje entre el código y el consumidor. Define clases aceptadas, si se permiten duplicados, cómo se resuelven conflictos y si el orden modifica el resultado.

Unidades de medida

Metros = Annotated[float, Unidad("m")]
Segundos = Annotated[float, Unidad("s")]

def velocidad(
    distancia: Metros,
    tiempo: Segundos,
) -> Annotated[float, Unidad("m/s")]:
    return distancia / tiempo

Para el analizador todos siguen siendo floats y pueden mezclarse. Usa NewType o clases de valor si quieres impedir esa mezcla. Annotated aporta metadatos, no distinción nominal.

Seguridad y datos no confiables

No uses Annotated como barrera de seguridad. Los consumidores pueden ignorar anotaciones y Python no las aplica automáticamente. Valida permisos, límites y formatos en runtime al entrar los datos.

Referencias futuras

get_type_hints() resuelve referencias usando namespaces. Sistemas de plugins, clases locales e importaciones condicionales pueden necesitar globalns y localns. La evaluación puede ejecutar resolución de nombres, por lo que metadatos de código no confiable no deben tratarse como datos inertes.

Conservar metadatos en decoradores

Los decoradores pueden sustituir anotaciones. Usa functools.wraps y evita sobrescribir __annotations__. ParamSpec conserva parámetros estáticamente, mientras Annotated debe seguir disponible para consumidores de runtime.

Serialización

Los objetos arbitrarios dentro de Annotated no son automáticamente serializables a JSON. Si deben enviarse a documentación, caches o servicios, prefiere dataclasses simples, enums y conversión explícita.

Compatibilidad de versiones

Annotated está disponible en versiones modernas de typing. Para versiones anteriores, usa typing_extensions.Annotated. Comprueba además que la biblioteca consumidora soporte include_extras=True.

Annotated frente a docstrings

Las docstrings son mejores para explicaciones largas y comportamiento general. Annotated es mejor para metadatos estructurados ligados a un parámetro o retorno. Evita strings ambiguos cuando una clase pequeña pueda expresar la regla.

Annotated frente a clase de valor

Una clase de valor puede validar en el constructor, ofrecer métodos y existir de forma distinta en runtime. Annotated conserva el valor original y depende de una herramienta. Usa clases para invariantes fuertes; usa Annotated para integración y descripción.

Errores comunes

  • Esperar validación automática: solo transporta metadatos.
  • Usar strings sin estructura: los errores de escritura son difíciles de detectar.
  • Olvidar include_extras=True: se pierden los metadatos.
  • Confiar en Annotated para seguridad: una llamada normal puede ignorarlo.
  • Usarlo para distinción nominal: dos ints anotados siguen siendo ints.
  • Acoplar el dominio a un framework: los tipos centrales pierden reutilización.

Ejemplo completo: configuración validada

from dataclasses import dataclass
from typing import Annotated, get_args, get_origin, get_type_hints

@dataclass(frozen=True)
class Minimo:
    valor: float

@dataclass(frozen=True)
class Maximo:
    valor: float

Puerto = Annotated[int, Minimo(1), Maximo(65535)]
Timeout = Annotated[float, Minimo(0.1), Maximo(120.0)]

@dataclass
class Configuracion:
    puerto: Puerto
    timeout: Timeout


def validar_dataclass(instancia: object) -> None:
    hints = get_type_hints(type(instancia), include_extras=True)
    for nombre, anotacion in hints.items():
        valor = getattr(instancia, nombre)
        if get_origin(anotacion) is not Annotated:
            continue
        tipo_base, *metadatos = get_args(anotacion)
        if not isinstance(valor, tipo_base):
            raise TypeError(f"{nombre}: tipo inválido")
        for item in metadatos:
            if isinstance(item, Minimo) and valor < item.valor:
                raise ValueError(f"{nombre}: debajo del mínimo")
            if isinstance(item, Maximo) and valor > item.valor:
                raise ValueError(f"{nombre}: encima del máximo")

config = Configuracion(puerto=8000, timeout=5.0)
validar_dataclass(config)

El ejemplo mantiene valores simples, centraliza metadatos y aplica una política explícita. Los validadores maduros también deben manejar herencia, opcionales, colecciones y múltiples errores.

Buenas prácticas

Usa objetos inmutables y nombrados. Documenta qué herramienta consume cada metadato. Mantén reglas críticas en código de runtime. Prueba la inspección en versiones soportadas. Evita distribuir objetos específicos de frameworks por todo el dominio.

Conclusión

typing.Annotated transporta metadatos junto a tipos sin cambiar su comportamiento estático básico. Es útil para validación, schemas, documentación, unidades, serialización e integración con frameworks.

La documentación oficial de Annotated en Python define su semántica. Trata los metadatos como un protocolo explícito entre código y consumidor, y usa NewType o clases de valor cuando necesites distinción o invariantes reales.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Macro shot capturing detailed patterns of a python in its natural surroundings.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Final en Python: protege constantes y herencia

    Aprende Final y @final en Python para proteger constantes, atributos, métodos y clases, comprendiendo los límites en runtime.

    Ler mais

    Tempo de leitura: 6 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

    NewType en Python: separa identificadores

    Aprende NewType en Python para separar IDs, códigos y valores primitivos, validar fronteras y evitar mezclas de dominio.

    Ler mais

    Tempo de leitura: 5 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