Field notes
Python 3.15 frozendict: Built-in Immutable Dictionaries
A look at Python 3.15's new built-in frozendict type (PEP 814)—shallow immutability, hashability, cache keys, and practical differences from standard dicts.
What is frozendict?
Python 3.15 introduces a new built-in type called frozendict (PEP 814). It is an immutable dictionary: once created, entries cannot be added, removed, or modified.
Until Python 3.15, achieving immutable mapping behavior required third-party packages, types.MappingProxyType (which is only a read-only view over a mutable dictionary), or custom tuple-of-tuples wrappers. frozendict makes immutable mappings first-class.
Basic usage
You cannot create a frozendict with curly-brace literal syntax {}; you instantiate it using the frozendict() constructor:
config = frozendict({
"host": "api.example.com",
"port": 8000,
})
print(config["host"]) # api.example.com
config["port"] = 9000 # Raises TypeError
config["debug"] = True # Raises TypeError
del config["host"] # Raises TypeError
Because mutation operations raise TypeError, configuration objects and shared constants remain protected from accidental in-place modifications.
Hashability and dictionary keys
A standard dict cannot be used as a dictionary key or placed inside a set because it is mutable and unhashable (TypeError: unhashable type: 'dict').
In contrast, if every key and value in a frozendict is hashable, the frozendict itself is hashable. This allows you to use complex structured configurations as dictionary keys, in sets, and as arguments to @functools.lru_cache:
from functools import lru_cache
settings = frozendict({"env": "prod", "region": "ap-south-1"})
# Use as dictionary keys
cache = {settings: "production-config"}
print(cache[settings])
# Use with lru_cache
@lru_cache(maxsize=128)
def compute_pipeline(parameters: frozendict):
# Safe from parameter mutation across cache hits
return run_complex_calculation(parameters)
Important implementation details
- Order preservation & equality: Like standard dictionaries,
frozendictpreserves insertion order. However, equality comparisons (==) check matching key-value pairs while ignoring order. - Inheritance:
frozendictis not a subclass ofdict; it inherits directly fromobject. - Type checking: Because it does not subclass
dict,isinstance(x, dict)evaluates toFalse. If your function needs to accept either mapping, writeisinstance(x, (dict, frozendict)), or usecollections.abc.Mapping. - Shallow immutability: Just like Python
tuple, immutability is shallow. If a value inside afrozendictis a mutable collection (such as a list), the nested object can still be modified:
data = frozendict({"tags": ["ai", "python"]})
# Allowed: The frozendict mapping is fixed, but the list itself is mutable
data["tags"].append("fastapi")
For completely immutable deep structures, ensure nested values are also immutable types (like tuple or nested frozendict).
Where frozendict is useful
- Immutable configuration and constants: Application settings, database connection profiles, and environment metadata that should never change at runtime.
- Safe cache keys: Clean compound keys for memoization without having to manually convert dictionaries into sorted tuple pairs.
- Function defaults: Avoids the infamous "mutable default argument" footgun in Python function definitions.
- AI agent & pipeline metadata: Passing immutable state, prompt metadata, or tool definitions through multi-stage agent workflows where intermediate handlers should not mutate upstream contracts.
Python 3.15 also introduces a sentinel builtin (PEP 661), but frozendict is the most significant addition to Python's core collection primitives.