Sometimes an application needs to share a dictionary without allowing consumers to mutate its structure directly. Copying the object may be expensive or may hide updates made by the owner. Exposing the original dictionary allows accidental changes. types.MappingProxyType provides a dynamic read-only view over an existing mapping.
This guide explains how to create proxies, distinguish read-only access from immutability, expose internal registries and configuration, handle nested mutable values, compare proxies with copies and snapshots, and avoid incorrect assumptions about security and concurrency.
What MappingProxyType does
MappingProxyType receives a mapping and returns an object that supports read operations while blocking mutation through the proxy.
from types import MappingProxyType
configuration = {
"host": "localhost",
"port": 8000,
}
public = MappingProxyType(configuration)
print(public["host"])
# public["port"] = 9000 # TypeErrorThe proxy supports mapping operations such as indexing, iteration, len(), get(), keys(), values(), and items(). Mutation methods are unavailable.
A dynamic view, not a copy
The proxy remains connected to the original mapping. Owner changes are immediately visible.
source = {"mode": "test"}
view = MappingProxyType(source)
print(view["mode"]) # test
source["mode"] = "production"
print(view["mode"]) # productionThis behavior is fundamental. MappingProxyType does not freeze the dictionary; it restricts mutation through a particular reference.
Read-only is not deeply immutable
The outer mapping cannot be changed through the proxy, but nested values keep their own mutability.
data = {
"users": ["Ana", "Bruno"],
"options": {"debug": False},
}
view = MappingProxyType(data)
view["users"].append("Carla")
view["options"]["debug"] = True
print(data)For deep immutability, normalize nested values to immutable types, take an appropriate deep copy, or use persistent immutable data structures.
Safely exposing internal state
A class can keep a mutable dictionary internally while exposing a proxy to consumers.
from collections.abc import Mapping
from types import MappingProxyType
class PluginRegistry:
def __init__(self) -> None:
self._plugins: dict[str, object] = {}
self._public = MappingProxyType(self._plugins)
@property
def plugins(self) -> Mapping[str, object]:
return self._public
def register(self, name: str, plugin: object) -> None:
if name in self._plugins:
raise ValueError(f"duplicate plugin: {name}")
self._plugins[name] = pluginConsumers may list and inspect plugins, but must use register() to change state. The class keeps control over validation and invariants.
Annotate readers as Mapping
Functions that only read data should normally accept collections.abc.Mapping instead of dict.
from collections.abc import Mapping
def build_url(config: Mapping[str, object]) -> str:
host = str(config["host"])
port = int(config["port"])
return f"http://{host}:{port}"This accepts dictionaries, mapping proxies, and other mapping implementations while communicating that mutation is not part of the contract.
Public configuration views
A common design builds a mutable configuration during startup and exposes a read-only view afterward.
_config = {
"timeout": 5.0,
"retries": 3,
"features": frozenset({"cache", "metrics"}),
}
CONFIG = MappingProxyType(_config)The uppercase name suggests a constant, and the proxy blocks accidental assignment by consumers. Code holding _config can still update the source.
Class namespaces use mapping proxies
Python exposes a class namespace through a mapping-proxy-like object in MyClass.__dict__. This prevents direct edits that would bypass internal class update mechanisms.
class Example:
value = 10
print(type(Example.__dict__))
print(Example.__dict__["value"])
# Example.__dict__["value"] = 20 # not allowed
Example.value = 20The proper assignment path lets the runtime maintain caches, descriptors, and invariants.
Comparison with dict.copy()
A shallow copy creates an independent outer dictionary.
source = {"a": 1}
copy = source.copy()
proxy = MappingProxyType(source)
source["a"] = 2
print(copy["a"]) # 1
print(proxy["a"]) # 2Use a copy when the consumer needs an independent snapshot. Use a proxy when the consumer should observe owner updates but must not mutate through the exposed reference.
Read-only snapshots
Combine copying and a proxy when both stability and read-only access are desired.
snapshot = MappingProxyType(dict(source))Later source changes do not appear. Nested mutable values are still shared because the copy is shallow.
Performance and memory
Creating a proxy is inexpensive because entries are not duplicated. This can matter for large registries exposed to many readers. Each access has a small extra layer of indirection, which is normally negligible compared with the encapsulation benefit.
Do not choose MappingProxyType purely for micro-optimization. Its main purpose is to make a read-only interface explicit and prevent accidental writes.
Concurrency and thread safety
A proxy does not make the underlying dictionary thread-safe. If another thread changes the source during iteration, readers may observe intermediate state or receive errors such as “dictionary changed size during iteration.”
for key, value in proxy.items():
... # avoid structural changes to the source hereUse locks, immutable snapshots, atomic reference replacement, or concurrency-specific structures when synchronization is required.
Serialization
Some libraries do not accept mappingproxy directly. Convert explicitly when an API requires a real dictionary.
import json
text = json.dumps(dict(proxy))This creates a shallow copy. Nested values must still be JSON-compatible.
Hashing and dictionary keys
A read-only proxy should not automatically be treated as a deeply immutable stable key. The source may change. When a stable hashable representation is needed, create one from immutable values.
key = tuple(sorted(source.items()))This only works if all contained values are hashable and order normalization is appropriate.
Common mistakes
- Assuming nested values are frozen: lists and dictionaries remain mutable.
- Expecting a snapshot: the proxy follows source updates.
- Using it as a security boundary: code with the original reference can still mutate.
- Assuming thread safety: no synchronization is provided.
- Annotating read-only consumers as dict: prefer Mapping.
- Repeatedly converting to dict: that removes the no-copy benefit.
Complete example: versioned catalog
from collections.abc import Mapping
from types import MappingProxyType
class Catalog:
def __init__(self) -> None:
self._items: dict[str, float] = {}
self._version = 0
self._view = MappingProxyType(self._items)
@property
def items(self) -> Mapping[str, float]:
return self._view
@property
def version(self) -> int:
return self._version
def set_price(self, code: str, price: float) -> None:
if price < 0:
raise ValueError("negative price")
self._items[code] = price
self._version += 1
catalog = Catalog()
catalog.set_price("A1", 39.90)
print(catalog.items["A1"])
# catalog.items["A1"] = 0 # TypeErrorThe catalog controls updates while readers see the current state through a non-mutating interface.
When another solution is better
Use frozenset for immutable sets, tuples for fixed sequences, frozen dataclasses for records with attributes, and persistent collections when functional updates must remain efficient. Returning a plain copy can be simpler when the mapping is small and isolation matters more than live updates.
For external configuration, validate and normalize data before exposing any public view.
Conclusion
types.MappingProxyType creates a dynamic read-only mapping view. It is useful for encapsulating internal dictionaries, exposing registries and configuration, and making consumer permissions clear.
The official Python MappingProxyType documentation describes its behavior. Use it with the understanding that it does not provide deep immutability, snapshots, security, or synchronization; those guarantees require additional architectural choices.







