typing.Required y typing.NotRequired controlan si cada clave debe estar presente en un TypedDict. Resuelven un problema frecuente: algunas claves son obligatorias y otras pueden omitirse, independientemente de que el valor acepte None.
Esta guía explica presencia frente a nulabilidad, total=True y total=False, modelos de creación, actualización y respuesta, herencia, ReadOnly, Unpack, validación de runtime, introspección, compatibilidad y errores comunes.
Qué controla total
Por defecto, todas las claves declaradas son obligatorias:
from typing import TypedDict
class Usuario(TypedDict):
id: int
nombre: str
usuario: Usuario = {"id": 1, "nombre": "Ana"}Omitir nombre debería producir un error estático. Con total=False, todas las claves declaradas en ese cuerpo son opcionales:
class ActualizarUsuario(TypedDict, total=False):
nombre: str
email: strEste modelo sirve para patches porque el cliente puede enviar solo los campos que desea cambiar.
Required dentro de un TypedDict parcial
from typing import Required, TypedDict
class Evento(TypedDict, total=False):
id: Required[str]
origen: Required[str]
detalles: dict[str, object]Aunque la clase sea parcial, id y origen deben existir. detalles puede omitirse. Required sobrescribe la regla general para una clave.
NotRequired dentro de un TypedDict total
from typing import NotRequired, TypedDict
class Producto(TypedDict):
id: int
nombre: str
descripcion: NotRequired[str]id y nombre son obligatorios. descripcion puede estar ausente. NotRequired permite mantener la clase total y marcar excepciones puntuales.
Ausente no es igual a None
class Perfil(TypedDict):
apodo: NotRequired[str]
biografia: str | Noneapodo puede faltar, pero cuando existe debe ser string. biografia debe existir, aunque su valor puede ser string o None. La diferencia es importante en JSON, bases de datos y APIs de actualización.
Tres estados útiles
class ActualizarPerfil(TypedDict, total=False):
apodo: str
biografia: str | NoneBiografía ausente significa “no cambiar”. None significa “borrar”. Una string significa “reemplazar”. Si se mezclan los tres estados, la API pierde información.
Comprobar claves opcionales
def aplicar(perfil: dict[str, object], patch: ActualizarPerfil) -> None:
if "apodo" in patch:
perfil["apodo"] = patch["apodo"]
if "biografia" in patch:
perfil["biografia"] = patch["biografia"]El test con in informa al analizador que la clave NotRequired está disponible en esa rama. Acceder sin comprobar puede generar advertencias y KeyError.
get y valores predeterminados
dict.get() evita KeyError, pero puede mezclar ausencia y None:
valor = patch.get("biografia")Si la diferencia importa, usa in o una sentinela:
AUSENTE = object()
valor = patch.get("biografia", AUSENTE)Modelos separados para cada operación
class CrearUsuario(TypedDict):
nombre: str
email: str
telefono: NotRequired[str]
class ActualizarUsuario(TypedDict, total=False):
nombre: str
email: str
telefono: str | None
class RespuestaUsuario(TypedDict):
id: int
nombre: str
email: str
telefono: str | NoneEl modelo de creación exige la entrada mínima. El patch acepta subconjuntos. La respuesta garantiza campos generados por el servidor.
Herencia para campos compartidos
class Identidad(TypedDict):
id: int
class DatosPublicos(TypedDict, total=False):
apodo: str
avatar: str
class UsuarioCompleto(Identidad, DatosPublicos):
nombre: strLa herencia combina grupos con totalidades diferentes. Mantén jerarquías pequeñas porque muchas capas dificultan conocer las claves realmente obligatorias.
Required y ReadOnly
from typing import ReadOnly
class Registro(TypedDict, total=False):
id: Required[ReadOnly[int]]
creado_en: Required[ReadOnly[str]]
nota: strid y creado_en deben existir y no deberían reasignarse. Presencia y escritura son dimensiones independientes. Consulta ReadOnly en Python.
Construcción dinámica
Un analizador puede no demostrar que un diccionario armado paso a paso contiene todas las claves:
datos = {}
datos["id"] = 1
datos["nombre"] = "Ana"
# asignar datos a Usuario puede fallarPrefiere literales completos, factories tipadas o una variable anotada desde el principio. Usa cast() solo cuando otra validación ya garantizó el contrato.
Validación de runtime
TypedDict no valida JSON ni datos externos. Un diccionario sin claves obligatorias sigue siendo un diccionario normal.
def es_usuario(valor: object) -> bool:
if not isinstance(valor, dict):
return False
return (
isinstance(valor.get("id"), int)
and isinstance(valor.get("nombre"), str)
)Para refinar el tipo después de validar, usa TypeGuard o TypeIs. Consulta TypeGuard en Python.
OpenAPI y schemas
Los frameworks pueden traducir Required y NotRequired a campos obligatorios y opcionales. El soporte varía con herencia y calificadores anidados. Inspecciona el schema generado y mantén pruebas de contrato.
Unpack para argumentos nombrados
from typing import Unpack
class Opciones(TypedDict, total=False):
timeout: float
cache: bool
def ejecutar(**opciones: Unpack[Opciones]) -> None:
...Required y NotRequired determinan qué argumentos nombrados son obligatorios cuando un TypedDict se expande con Unpack.
Compatibilidad entre TypedDicts
Una estructura donde una clave es obligatoria no siempre es compatible con otra donde la misma clave es opcional. El consumidor del tipo opcional podría eliminarla o escribir de forma incompatible. Las reglas consideran presencia, tipo, escritura y herencia.
Sintaxis funcional para claves especiales
Cabeceras = TypedDict(
"Cabeceras",
{
"content-type": Required[str],
"x-request-id": NotRequired[str],
},
)La sintaxis funcional permite claves que no son identificadores Python, como nombres con guiones.
Introspección
print(Producto.__required_keys__)
print(Producto.__optional_keys__)Estos conjuntos describen la presencia efectiva. Son útiles para herramientas, pero no sustituyen la validación de valores y estructuras anidadas.
__total__ no cuenta toda la historia
__total__ indica solo la totalización declarada en el cuerpo actual. Una clase con valor verdadero puede tener claves NotRequired o heredar claves opcionales. Usa los conjuntos de claves para conocer el contrato real.
Evolución de contratos
Convertir una clave obligatoria en opcional puede romper consumidores que accedían sin comprobar. Convertir una opcional en obligatoria rompe productores antiguos. Usa versiones de schema, migraciones, defaults y pruebas de compatibilidad.
Errores comunes
- Usar Optional para indicar ausencia:
T | Nonecontrola el valor. - Leer NotRequired sin comprobar: puede ocurrir KeyError.
- Reutilizar respuestas como patches: obliga a enviar campos indebidos.
- Confiar en TypedDict en runtime: los datos externos necesitan validación.
- Crear herencias profundas: el contrato real se vuelve confuso.
- Usar get cuando None y ausencia difieren: se pierde semántica.
Ejemplo completo: configuración de tarea
from typing import NotRequired, Required, TypedDict
class Tarea(TypedDict, total=False):
nombre: Required[str]
comando: Required[list[str]]
directorio: str
timeout: float
reintentos: int
entorno: dict[str, str]
descripcion: NotRequired[str]
def ejecutar(tarea: Tarea) -> None:
nombre = tarea["nombre"]
comando = tarea["comando"]
directorio = tarea.get("directorio", ".")
timeout = tarea.get("timeout", 30.0)
reintentos = tarea.get("reintentos", 1)
print(nombre, comando, directorio, timeout, reintentos)Las dos claves esenciales son Required. Las demás pueden omitirse y recibir valores predeterminados. El contrato permanece legible sin dividirlo en muchas clases.
Buenas prácticas
Modela presencia separadamente de nulabilidad. Define tipos diferentes para crear, actualizar y responder. Usa in cuando ausencia tenga significado. Valida en las fronteras. Ejecuta mypy, pyright u otro analizador en CI. Prueba payloads mínimos, completos, nulos e inválidos.
Conclusión
Required y NotRequired hacen TypedDict más preciso al controlar cada clave. Representan payloads reales sin confundir datos ausentes con None y sin obligar a todas las claves a seguir la misma totalización.
La documentación oficial de Required y NotRequired en Python define las reglas. Usa los calificadores para contratos estáticos claros y compleméntalos con validación de runtime en cualquier frontera no confiable.







