Python MappingProxyType: Read-Only Dict

Published on: August 28, 2026
Reading time: 4 minutes
Close-up view of a computer screen displaying code in a software development environment.

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  # TypeError

The 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"])  # production

This 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] = plugin

Consumers 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 = 20

The 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"])  # 2

Use 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 here

Use 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  # TypeError

The 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Close-up of a metal gate featuring a 'Please Keep Gate Closed' sign, outdoors.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Literal: Restrict Exact Values

    Learn Python Literal to restrict exact values, create overloads, discriminate TypedDict variants, use match/case, and improve typed APIs.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypedDict: Typed Dictionaries

    Learn Python TypedDict for typed dictionaries, optional keys, NotRequired, Required, API payloads, discriminated variants, and static safety.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Protocol: Structural Typing

    Learn Python Protocol for structural typing, generic contracts, callbacks, runtime_checkable, testing, dependency injection, and low coupling.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python memoryview: Zero-Copy Buffers

    Learn Python memoryview for zero-copy buffers, slices, writable bytearray data, cast, mmap, struct, sockets, and safe lifecycle management.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tracemalloc: Track Memory

    Learn Python tracemalloc to measure peaks, create and compare snapshots, filter allocations, and diagnose memory growth and leaks.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Commuters line up at a subway station platform, showcasing public transportation dynamics.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python queue: Communicate Between Threads

    Learn Python queue for thread communication with FIFO, LIFO, priority, backpressure, task tracking, sentinels, and safe shutdown.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026