Read-Only Dictionaries

Passing a dict to a function hands over the same object you hold, so every write the function makes lands in your dict. A shared settings dict gets changed this way by code that only meant to read it:

DEFAULTS = {"timeout": 30, "retries": 3}

def build_request(settings):
    settings["timeout"] = 5
    return f"GET /feed timeout={settings['timeout']}"

print(build_request(DEFAULTS))
print(DEFAULTS)

Output:

GET /feed timeout=5
{'timeout': 5, 'retries': 3}

Every later reader of DEFAULTS now gets a 5-second timeout, and nothing in the error logs points back at build_request. You have 2 ways to stop it: give the function its own copy, or give it a view that refuses writes.

A view that refuses writes

The standard library's types module provides MappingProxyType. Wrapping a dict in it gives a read-only view. Every read still works: indexing and iteration, plus get, in, len, keys(), values() and items(). Any attempt to assign or delete raises TypeError at that line.

from types import MappingProxyType

settings = {"timeout": 30, "retries": 3}
view = MappingProxyType(settings)

print(view["timeout"], view.get("region", "us-east"), len(view))
print("retries" in view, list(view.items()))

try:
    view["timeout"] = 5
except TypeError as err:
    print("TypeError:", err)

try:
    del view["retries"]
except TypeError as err:
    print("TypeError:", err)

Output:

30 us-east 2
True [('timeout', 30), ('retries', 3)]
TypeError: 'mappingproxy' object does not support item assignment
TypeError: 'mappingproxy' object does not support item deletion

The mutating methods are missing from the view entirely. pop and update are gone, so are setdefault and clear, and calling any of them raises AttributeError. Either way the bad write fails at the line that attempted it, which is where you want to find the bug.

What happens when this program runs?

from types import MappingProxyType

config = {"mode": "fast"}
view = MappingProxyType(config)
print(view.get("mode"))
view.pop("mode")
print("done")

The view follows its dict

A proxy reads through to the dict it wraps and stores nothing itself. The usual design keeps the real dict private to its owner, who can still change it, while every holder of the view sees the new value on its next read. For a snapshot that nobody can change, the owner included, wrap a copy instead.

from types import MappingProxyType

_limits = {"max_upload_mb": 100}
LIMITS = MappingProxyType(_limits)

_limits["max_upload_mb"] = 250
print(LIMITS["max_upload_mb"])

frozen = MappingProxyType(dict(_limits))
_limits["max_upload_mb"] = 500
print(LIMITS["max_upload_mb"], frozen["max_upload_mb"])

Output:

250
500 250

Read-only stops at the first level

The proxy blocks writes to its own keys only. A value stored under a key is the same object the wrapped dict holds, so a nested dict or list stays writable through the view. To lock nested dicts as well, rebuild each one as a proxy on the way in. Lists inside still need copying if callers must not change them.

from types import MappingProxyType

def freeze(d):
    return MappingProxyType(
        {k: freeze(v) if isinstance(v, dict) else v for k, v in d.items()}
    )

raw = {"db": {"host": "primary", "port": 5432}, "replicas": ["r1"]}
shallow = MappingProxyType(raw)
shallow["db"]["host"] = "replica"
print(raw["db"]["host"])

deep = freeze(raw)
try:
    deep["db"]["host"] = "primary"
except TypeError as err:
    print("blocked:", err)
deep["replicas"].append("r2")
print(raw["replicas"])

Output:

replica
blocked: 'mappingproxy' object does not support item assignment
['r1', 'r2']
Pass a copy: dict(d)
  • The callee can change its copy, and your dict stays as it was.
  • A write in the callee goes unnoticed, so a logic bug there stays hidden.
  • Each call pays for a full copy of the dict.
Pass a view: MappingProxyType(d)
  • Any write raises TypeError at the line that tried it.
  • No copy is made, so wrapping a large dict costs almost nothing.
  • The owner's later changes show through, and nested values stay writable.

What does this program print?

from types import MappingProxyType

prices = {"basic": 10}
public = MappingProxyType(prices)
prices["pro"] = 25
snapshot = dict(public)
prices["basic"] = 12
print(len(public), snapshot["basic"], public["basic"])

About this lesson

This section is part of the Dictionaries: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.