Required y NotRequired: campos opcionales en TypedDict

Publicado el: 29/08/2026
Tempo de leitura: 5 minutos
Person holding Python logo sticker with blurred background, highlighting programming focus.

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: str

Este 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 | None

apodo 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 | None

Biografí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 | None

El 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: str

La 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: str

id 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 fallar

Prefiere 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 | None controla 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Ball python slithering on a sunlit gravel pathway outdoors.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ReadOnly en Python: protege campos TypedDict

    Aprende ReadOnly en Python para proteger campos TypedDict, modelar contratos estables y evitar escrituras accidentales.

    Ler mais

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

    TypeAliasType en Python: alias en runtime

    Aprende TypeAliasType en Python para crear alias explícitos, inspeccionarlos en runtime y modelar APIs genéricas reutilizables.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    A person reads 'Python for Unix and Linux System Administration' indoors.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    overload en Python: firmas precisas

    Aprende typing.overload en Python para firmas precisas con Literal, None, genéricos, métodos y retornos dependientes de argumentos.

    Ler mais

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

    ClassVar en Python: separa clase e instancia

    Aprende ClassVar en Python para separar atributos de clase e instancia en dataclasses, registries, caches, herencia y contadores.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    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
    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