typing.ReadOnly permite declarar claves de solo lectura dentro de un TypedDict. Es útil cuando una estructura con forma de diccionario contiene campos que pueden definirse al crear el registro, pero que no deberían reemplazarse después. Algunos ejemplos habituales son identificadores, fechas de creación, metadatos de despliegue, códigos de transacción y valores producidos por servicios confiables.
En esta guía aprenderás qué significa ReadOnly, cómo lo interpretan los verificadores de tipos, por qué no convierte el diccionario en un objeto inmutable durante la ejecución y cómo aplicarlo de manera segura en proyectos reales.
El problema que resuelve ReadOnly
Un TypedDict describe la forma esperada de un diccionario. Indica qué claves existen y qué tipo de valor acepta cada una. Sin claves de solo lectura, todos los campos suelen considerarse modificables desde el punto de vista del análisis estático. Eso dificulta expresar un contrato frecuente: el registro puede cambiar, pero algunos valores deben permanecer estables.
Piensa en un usuario. El nombre y el correo pueden actualizarse, mientras que el identificador interno no debería cambiar después de crear el registro.
from typing import ReadOnly, TypedDict
class Usuario(TypedDict):
id: ReadOnly[int]
nombre: str
email: str
El verificador permite leer usuario['id'], pero marca como error una asignación como usuario['id'] = 99. Las demás claves siguen siendo editables.
Protección estática, no inmutabilidad real
ReadOnly no cambia la implementación del diccionario. Durante la ejecución continúa siendo un dict normal. Python no bloquea por sí mismo una asignación a la clave anotada. La protección aparece en herramientas como Pyright, mypy, inspecciones del editor o comprobaciones ejecutadas en integración continua.
Esta diferencia es esencial. La tipificación comprueba el uso previsto antes de ejecutar el programa, mientras que la validación comprueba los datos reales en tiempo de ejecución. Los proyectos sólidos suelen necesitar ambas capas.
Ejemplo con configuración
Una configuración de aplicación puede mezclar valores fijos y ajustables. El entorno y la versión de despliegue pueden ser estables, mientras que el nivel de registro y el tiempo de espera pueden cambiar.
from typing import ReadOnly, TypedDict
class Configuracion(TypedDict):
entorno: ReadOnly[str]
version: ReadOnly[str]
nivel_log: str
timeout: float
config: Configuracion = {
'entorno': 'produccion',
'version': '2.4.0',
'nivel_log': 'INFO',
'timeout': 10.0,
}
config['nivel_log'] = 'DEBUG'
Cambiar el nivel de log es válido. Reemplazar el entorno o la versión debería generar un error de tipado. La definición comunica claramente el ciclo de vida de cada campo.
Claves opcionales de solo lectura
Una clave de solo lectura también puede ser opcional. Utiliza NotRequired cuando el campo puede no estar presente, pero no debe reemplazarse después de aparecer.
from typing import NotRequired, ReadOnly, TypedDict
class RespuestaAPI(TypedDict):
request_id: ReadOnly[str]
resultado: str
cache_key: NotRequired[ReadOnly[str]]
En este caso, cache_key puede faltar. Cuando existe, los consumidores deben tratarla como metadato estable. Este diseño encaja bien con respuestas HTTP, eventos, mensajes y proyecciones de base de datos.
Contratos de funciones
Las claves de solo lectura mejoran las firmas de funciones porque dejan claro qué partes de la estructura pueden consultarse sin ser reemplazadas.
class Pedido(TypedDict):
codigo: ReadOnly[str]
estado: str
total: float
def marcar_pagado(pedido: Pedido) -> None:
pedido['estado'] = 'pagado'
# pedido['codigo'] = 'otro' # error de tipado
El contrato es más preciso que una anotación genérica con dict. El código representa identidad, mientras que el estado está diseñado para evolucionar.
Tipado estructural y alias
TypedDict utiliza tipado estructural. La compatibilidad depende de las claves disponibles y de sus tipos, no solo del nombre de la clase. Los calificadores de solo lectura participan en ese análisis, especialmente cuando varios alias hacen referencia al mismo diccionario.
Evita conversiones amplias con cast que eliminen la información de solo lectura. Si necesitas adaptar datos, considera crear una copia o definir un tipo de entrada más específico. Deja que el verificador compruebe si la relación es segura.
ReadOnly frente a dataclasses congeladas
ReadOnly no sustituye a @dataclass(frozen=True). Una dataclass congelada ofrece una barrera en tiempo de ejecución contra la asignación normal de atributos. También admite métodos, propiedades y comportamiento de dominio. Un TypedDict sigue siendo ideal cuando los datos deben mantener semántica de diccionario, como JSON, configuraciones y cargas HTTP.
Usa claves de solo lectura cuando solo algunos campos requieren protección estática. Elige una clase inmutable cuando todo el objeto deba comportarse como un valor y la protección en ejecución sea importante.
Validación en tiempo de ejecución
Las entradas externas deben validarse aunque el tipo use ReadOnly. Las anotaciones no verifican automáticamente el JSON recibido por red, los datos de un archivo ni los diccionarios creados por código sin tipos.
def crear_usuario(datos: dict[str, object]) -> Usuario:
identificador = datos.get('id')
nombre = datos.get('nombre')
email = datos.get('email')
if not isinstance(identificador, int):
raise ValueError('id invalido')
if not isinstance(nombre, str) or not isinstance(email, str):
raise ValueError('datos invalidos')
return {'id': identificador, 'nombre': nombre, 'email': email}
La función valida los valores reales y devuelve una estructura compatible con el contrato estático. En sistemas grandes puedes usar una biblioteca de validación, pero la separación sigue siendo la misma.
Compatibilidad entre versiones
Antes de adoptar la función, confirma la versión mínima de Python y el soporte del verificador elegido. Los proyectos que funcionan en versiones anteriores pueden usar una implementación compatible desde typing_extensions. Mantén actualizado el intérprete y las herramientas de análisis.
Consulta la documentación oficial de typing y la especificación de PEP 705 para conocer las reglas formales.
Buenas prácticas de diseño
Reserva ReadOnly para campos que representen identidad, procedencia, metadatos generados o invariantes importantes. No marques todas las claves automáticamente. Si casi toda la estructura debe ser inmutable, probablemente una dataclass congelada sea más clara.
No uses cast solo para ocultar errores. Investiga si la modificación es realmente válida. Centraliza la construcción y la validación, documenta cuándo se define cada valor estable y ejecuta el verificador en integración continua.
Migración gradual
Introduce la anotación poco a poco. Empieza por identificadores y fechas que ya se consideran estables por convención. Ejecuta el verificador, corrige las mutaciones reales y añade pruebas sobre los flujos de creación y actualización. Así evitas que una migración masiva oculte problemas de diseño.
Cuando un proceso necesita legítimamente otro valor, crea un nuevo registro o utiliza tipos diferentes para distintas etapas del ciclo de vida. Por ejemplo, un borrador y un registro persistido pueden tener contratos separados.
Recursos relacionados
Para ampliar el tema, revisa en Academify las guías sobre typing en Python, dataclasses en Python, diccionarios en Python y JSON en Python. Estos contenidos ayudan a elegir entre mapeos tipados, clases de datos y validación de entradas.
Conclusión
typing.ReadOnly hace que los contratos de TypedDict sean más expresivos. Permite distinguir claves estables de campos editables, ayuda a detectar reemplazos accidentales y mejora la documentación de APIs basadas en diccionarios.
Como la protección es estática, debe combinarse con validación, pruebas y un verificador ejecutado de forma consistente. Usado de manera selectiva, ofrece una solución ligera para conservar invariantes importantes sin perder la compatibilidad y la comodidad de los diccionarios de Python.







