Los diccionarios de Python son flexibles, pero esa flexibilidad puede ocultar errores. Un payload puede exigir id, nombre y activo, mientras el código describe todo como dict[str, object]. Con ese tipo genérico, el analizador no sabe qué claves existen, cuáles son opcionales ni qué tipo corresponde a cada clave. typing.TypedDict permite describir la estructura esperada de un diccionario sin cambiar su comportamiento en runtime.
Esta guía cubre declaraciones básicas, claves obligatorias y opcionales, NotRequired, Required, herencia, payloads anidados, variantes discriminadas y la diferencia entre tipado estático y validación real.
Qué representa TypedDict
TypedDict es una declaración de tipo para diccionarios con claves conocidas. Durante la ejecución, el valor sigue siendo un dict normal.
from typing import TypedDict
class Usuario(TypedDict):
id: int
nombre: str
activo: bool
usuario: Usuario = {
"id": 10,
"nombre": "Ana",
"activo": True,
}Mypy o Pyright puede detectar una clave ausente, un nombre mal escrito o un valor incompatible.
usuario_erroneo: Usuario = {
"id": "10", # se esperaba int
"nombre": "Ana",
"activo": True,
}Python no impide esa asignación durante la ejecución. La protección aparece al ejecutar un analizador estático.
Por qué dict[str, object] no basta
dict[str, object] solo indica que las claves son strings y los valores pueden ser cualquier objeto. No relaciona cada clave con un tipo útil.
def mostrar(usuario: dict[str, object]) -> str:
return usuario["nombre"].upper() # object no garantiza upper()Con TypedDict, el analizador sabe que usuario["nombre"] es str.
def mostrar(usuario: Usuario) -> str:
return usuario["nombre"].upper()Esta precisión amplía lo explicado en la guía de type hints en Python.
Claves opcionales con total=False
Por defecto, todas las claves son obligatorias. Usa total=False cuando todas puedan faltar.
class ActualizacionUsuario(TypedDict, total=False):
nombre: str
activo: bool
email: strEs útil para operaciones PATCH, donde el cliente envía solo los campos que desea modificar.
def actualizar_usuario(usuario_id: int, datos: ActualizacionUsuario) -> None:
if "nombre" in datos:
print(datos["nombre"])Una clave opcional no significa que su valor acepte None. La clave puede faltar; si existe, debe contener el tipo declarado.
NotRequired y Required
Usa NotRequired cuando solo algunos campos puedan faltar. Usa Required dentro de un TypedDict con total=False cuando una clave deba seguir siendo obligatoria.
from typing import NotRequired, Required, TypedDict
class Perfil(TypedDict):
id: int
nombre: str
apodo: NotRequired[str]
foto: NotRequired[str]
class EventoParcial(TypedDict, total=False):
tipo: Required[str]
payload: object
origen: strEstos marcadores mantienen el contrato legible sin crear varios tipos casi idénticos.
Herencia y composición
Un TypedDict puede heredar campos de otro TypedDict.
class Entidad(TypedDict):
id: int
class Producto(Entidad):
nombre: str
precio: float
stock: intLa herencia funciona bien para grupos pequeños de campos comunes. Sin embargo, las jerarquías complejas dificultan la evolución. Separa los esquemas de creación, actualización, respuesta y persistencia cuando tengan requisitos distintos.
Sintaxis funcional
La forma funcional es útil cuando las claves no son identificadores Python válidos.
Cabeceras = TypedDict(
"Cabeceras",
{
"content-type": str,
"x-request-id": str,
},
)Para nombres normales, la sintaxis de clase suele ser más clara y fácil de documentar.
Modelar respuestas de APIs
TypedDict es apropiado para payloads internos o respuestas externas después de validarlas.
class Direccion(TypedDict):
ciudad: str
provincia: str
codigo_postal: str
class ClienteAPI(TypedDict):
id: int
nombre: str
direccion: Direccion
etiquetas: list[str]
def ciudad_cliente(cliente: ClienteAPI) -> str:
return cliente["direccion"]["ciudad"]No anotes directamente response.json() como un TypedDict confiable. Las respuestas pueden omitir claves, cambiar formatos o contener valores inesperados.
TypedDict no valida en runtime
La declaración no convierte valores, no rechaza claves extra y no produce errores de validación. JSON, formularios, colas y registros de base de datos siguen necesitando validación real.
import json
texto = '{"id": "diez", "nombre": 99, "activo": true}'
datos = json.loads(texto)
# datos existe aunque sea incompatible con UsuarioUna frontera segura valida el objeto primero y solo después devuelve o refina el tipo. Pydantic, dataclasses con parsing explícito, attrs o validadores propios pueden cumplir esa función.
Refinar claves opcionales
Cuando un campo usa NotRequired, comprueba su presencia antes de indexarlo.
class Resultado(TypedDict):
valor: int
aviso: NotRequired[str]
def imprimir(resultado: Resultado) -> None:
print(resultado["valor"])
if "aviso" in resultado:
print(resultado["aviso"])get() también está disponible, pero su resultado normalmente incluye None y exige tratamiento.
Compatibilidad estructural y claves extra
La compatibilidad de TypedDict es estructural. Un tipo con más campos puede aceptarse donde se espera una forma menor, pero la obligatoriedad, la mutabilidad y los tipos de valor imponen restricciones.
Evita depender de reglas poco intuitivas. Mantén los contratos pequeños y ejecuta el mismo analizador en desarrollo y CI.
TypedDict como entrada y salida
class NuevoPedido(TypedDict):
cliente_id: int
items: list[int]
cupon: NotRequired[str]
class PedidoCreado(TypedDict):
id: int
estado: str
total: float
def crear_pedido(datos: NuevoPedido) -> PedidoCreado:
return {
"id": 501,
"estado": "creado",
"total": 149.90,
}La firma documenta la frontera de la función y mejora autocompletado, revisión y refactorización.
Variantes discriminadas con Literal
TypedDict combina especialmente bien con Literal.
from typing import Literal
class Exito(TypedDict):
tipo: Literal["exito"]
valor: int
class Fallo(TypedDict):
tipo: Literal["fallo"]
error: str
Respuesta = Exito | Fallo
def procesar(respuesta: Respuesta) -> str:
if respuesta["tipo"] == "exito":
return str(respuesta["valor"])
return respuesta["error"]El discriminador permite que el analizador reduzca la unión a la forma correcta.
Evolución de esquemas
Al evolucionar una API, añadir campos opcionales suele ser menos disruptivo que cambiar claves obligatorias. Eliminar un campo o modificar su tipo debería versionarse o migrarse con cuidado. TypedDict ayuda a localizar consumidores afectados.
El ecosistema moderno de typing también incorpora conceptos de campos de solo lectura. Verifica el soporte en la versión de Python y el analizador del proyecto antes de usar características recientes en una biblioteca pública.
Errores comunes
- Usar TypedDict como validación JSON: solo aporta metadatos estáticos.
- Confundir ausencia con None: opcionalidad de clave y valor nullable son distintos.
- Usar un esquema para todas las operaciones: creación, patch y respuesta suelen necesitar tipos separados.
- Aplicar cast a datos no validados: el cast silencia el analizador, pero no corrige valores.
- Declarar todos los campos como object: se pierde casi toda la precisión.
- Ignorar obligatoriedad y mutabilidad: la compatibilidad estructural también tiene reglas.
TypedDict, dataclass o Pydantic
Elige TypedDict cuando el valor de runtime debe seguir siendo un diccionario y la necesidad principal es el análisis estático. Elige dataclass cuando quieras objetos con atributos, métodos y construcción explícita. Elige Pydantic cuando debas validar, convertir o documentar datos externos.
Las herramientas pueden coexistir. Una capa de entrada puede validar con Pydantic y luego entregar estructuras TypedDict ligeras a código que integra bibliotecas basadas en diccionarios.
Conclusión
typing.TypedDict convierte diccionarios informales en contratos verificables. Describe claves obligatorias, opcionales, estructuras anidadas y variantes discriminadas sin alterar el objeto en runtime.
La documentación oficial de TypedDict en Python cubre totality, herencia y marcadores de obligatoriedad. Úsalo para formas conocidas, ejecuta un analizador estático y conserva validación real en todas las fronteras no confiables.







