A veces una aplicación necesita compartir un diccionario sin permitir que los consumidores modifiquen su estructura directamente. Copiar el objeto puede ser costoso o puede ocultar actualizaciones realizadas por el propietario. Exponer el diccionario original permite cambios accidentales. types.MappingProxyType crea una vista dinámica de solo lectura sobre un mapping existente.
Esta guía explica cómo crear proxies, diferenciar acceso de solo lectura e inmutabilidad, exponer registros internos y configuración, manejar valores mutables anidados, comparar proxies con copias y snapshots, y evitar expectativas incorrectas sobre seguridad y concurrencia.
Qué hace MappingProxyType
MappingProxyType recibe un mapping y devuelve un objeto que permite operaciones de lectura, pero bloquea la mutación a través del proxy.
from types import MappingProxyType
configuracion = {
"host": "localhost",
"puerto": 8000,
}
publica = MappingProxyType(configuracion)
print(publica["host"])
# publica["puerto"] = 9000 # TypeErrorEl proxy admite indexación, iteración, len(), get(), keys(), values() e items(). Los métodos de mutación no están disponibles.
Una vista dinámica, no una copia
El proxy permanece conectado al mapping original. Los cambios del propietario aparecen inmediatamente.
origen = {"modo": "prueba"}
vista = MappingProxyType(origen)
print(vista["modo"]) # prueba
origen["modo"] = "produccion"
print(vista["modo"]) # produccionMappingProxyType no congela el diccionario; restringe la mutación a través de una referencia concreta.
Solo lectura no significa inmutabilidad profunda
La estructura externa no puede cambiarse mediante el proxy, pero los valores anidados conservan su mutabilidad.
datos = {
"usuarios": ["Ana", "Bruno"],
"opciones": {"debug": False},
}
vista = MappingProxyType(datos)
vista["usuarios"].append("Carla")
vista["opciones"]["debug"] = True
print(datos)Para inmutabilidad profunda, normaliza los valores a tipos inmutables, realiza una copia profunda cuando corresponda o usa estructuras persistentes diseñadas para ese fin.
Exponer estado interno con seguridad
Una clase puede mantener un diccionario mutable internamente y exponer un proxy.
from collections.abc import Mapping
from types import MappingProxyType
class RegistroPlugins:
def __init__(self) -> None:
self._plugins: dict[str, object] = {}
self._publico = MappingProxyType(self._plugins)
@property
def plugins(self) -> Mapping[str, object]:
return self._publico
def registrar(self, nombre: str, plugin: object) -> None:
if nombre in self._plugins:
raise ValueError(f"plugin duplicado: {nombre}")
self._plugins[nombre] = pluginLos consumidores pueden consultar plugins, pero deben usar registrar() para cambiar el estado. La clase conserva sus validaciones e invariantes.
Anotar lectores como Mapping
Las funciones que solo leen deberían aceptar collections.abc.Mapping en vez de dict.
from collections.abc import Mapping
def construir_url(config: Mapping[str, object]) -> str:
host = str(config["host"])
puerto = int(config["puerto"])
return f"http://{host}:{puerto}"Así se aceptan diccionarios, proxies y otros mappings, comunicando que la mutación no forma parte del contrato.
Configuración pública y privada
Un patrón común construye una configuración mutable durante el inicio y expone una vista de lectura.
_config = {
"timeout": 5.0,
"reintentos": 3,
"features": frozenset({"cache", "metricas"}),
}
CONFIG = MappingProxyType(_config)El nombre en mayúsculas sugiere una constante y el proxy bloquea asignaciones accidentales. El código que conserva _config todavía puede modificar el origen.
Namespaces de clase
Python expone el namespace de una clase mediante un objeto parecido a mappingproxy en MiClase.__dict__. Esto evita ediciones directas que ignorarían mecanismos internos.
class Ejemplo:
valor = 10
print(type(Ejemplo.__dict__))
print(Ejemplo.__dict__["valor"])
# Ejemplo.__dict__["valor"] = 20 # no permitido
Ejemplo.valor = 20La asignación correcta permite que el runtime mantenga caches, descriptores e invariantes.
Comparación con dict.copy()
Una copia superficial crea un diccionario externo independiente.
origen = {"a": 1}
copia = origen.copy()
proxy = MappingProxyType(origen)
origen["a"] = 2
print(copia["a"]) # 1
print(proxy["a"]) # 2Usa una copia cuando el consumidor necesite un snapshot independiente. Usa un proxy cuando deba observar cambios del propietario sin poder modificar la referencia expuesta.
Snapshots de solo lectura
Combina copia y proxy cuando quieras estabilidad y protección.
snapshot = MappingProxyType(dict(origen))Los cambios posteriores en el origen no aparecen. Los objetos anidados siguen compartidos porque la copia es superficial.
Rendimiento y memoria
Crear un proxy es barato porque no duplica todas las entradas. Puede ser útil para registros grandes compartidos con muchos lectores. El acceso añade una pequeña indirección, normalmente irrelevante frente al beneficio de encapsulación.
No elijas MappingProxyType solamente por microoptimización. Su principal valor es expresar una interfaz de lectura y bloquear escrituras accidentales.
Concurrencia y thread safety
Un proxy no vuelve thread-safe al diccionario original. Si otra thread cambia el origen durante una iteración, los lectores pueden observar un estado intermedio o recibir “dictionary changed size during iteration”.
for clave, valor in proxy.items():
... # evita cambios estructurales simultáneosUsa locks, snapshots, sustitución atómica de referencias o estructuras específicas cuando necesites sincronización.
Serialización
Algunas bibliotecas no aceptan mappingproxy directamente. Convierte cuando una API exija un diccionario real.
import json
texto = json.dumps(dict(proxy))La conversión crea una copia superficial y los valores anidados deben ser serializables.
Hash y uso como clave
Un proxy de solo lectura no debe considerarse automáticamente una clave profundamente inmutable. El origen puede cambiar. Para una representación estable, crea una estructura inmutable a partir de valores hashable.
clave = tuple(sorted(origen.items()))Esto funciona solo si todos los valores son hashable y la normalización por orden es adecuada.
Errores comunes
- Creer que los valores anidados quedan congelados: listas y diccionarios siguen mutables.
- Esperar un snapshot: el proxy sigue los cambios del origen.
- Usarlo como frontera de seguridad: quien posee la referencia original puede modificar.
- Suponer thread safety: no existe sincronización automática.
- Anotar lectores como dict: usa Mapping.
- Convertir repetidamente a dict: se pierde la ventaja de evitar copias.
Ejemplo completo: catálogo versionado
from collections.abc import Mapping
from types import MappingProxyType
class Catalogo:
def __init__(self) -> None:
self._items: dict[str, float] = {}
self._version = 0
self._vista = MappingProxyType(self._items)
@property
def items(self) -> Mapping[str, float]:
return self._vista
@property
def version(self) -> int:
return self._version
def definir_precio(self, codigo: str, precio: float) -> None:
if precio < 0:
raise ValueError("precio negativo")
self._items[codigo] = precio
self._version += 1
catalogo = Catalogo()
catalogo.definir_precio("A1", 39.90)
print(catalogo.items["A1"])
# catalogo.items["A1"] = 0 # TypeErrorEl catálogo controla las actualizaciones mientras los lectores observan el estado actual mediante una interfaz no mutable.
Cuándo usar otra solución
Usa frozenset para conjuntos inmutables, tuplas para secuencias fijas, dataclasses congeladas para registros con atributos y colecciones persistentes para actualizaciones funcionales eficientes. Una copia simple puede ser mejor si el mapping es pequeño y el aislamiento importa más que ver cambios en vivo.
Conclusión
types.MappingProxyType crea una vista dinámica de solo lectura sobre un mapping. Es útil para encapsular diccionarios internos, exponer registros y configuración, y comunicar permisos claros a los consumidores.
La documentación oficial de MappingProxyType en Python describe su comportamiento. Úsalo sabiendo que no ofrece inmutabilidad profunda, snapshot, seguridad ni sincronización; esas garantías requieren decisiones adicionales.







