Python 3.15 adds a frozendict built-in, giving dictionaries the immutable counterpart that lists and sets have had for years. Freezing a mapping buys you two things. You get safety, since nothing can deliberately or accidentally mutate it once it’s built. You also get hashability, which opens doors that stay shut for a regular dict. In this tutorial, you’ll use a frozen mapping as a dictionary key, a set member, and a cache argument.
By the end of this tutorial, you’ll understand that:
- Python 3.15 ships
frozendictas a built-in type, so you never have to import anything to use it. - A
frozendictis hashable only when all of its keys and values are hashable. frozendictdoesn’t inherit fromdict, so your existingisinstance()checks won’t accept one.- The freeze is shallow, so a nested list stays mutable and drags the whole mapping’s hashability down with it.
- The augmented assignment operator (
|=) rebinds your variable to a new object instead of mutating the old one.
Along the way, you’ll trace the fourteen-year road that three separate PEPs took to get here, watch the mutation methods vanish from a real REPL session, and work through the cases where reaching for a frozen mapping pays off. You’ll also see exactly where immutability has its limits.
Note: The examples in this tutorial use Python 3.15.0rc2, so some details may shift slightly before the final release on October 1.
Get Your Code: Click here to download the free sample code you’ll use to build immutable mappings you can hash, cache, and share across threads with Python 3.15.
Get to Know Python 3.15’s New frozendict Type
Python’s dictionaries have long been the odd ones out among the built-in data containers. For example, you can think of a tuple as a read-only counterpart to a list and a frozenset as an immutable counterpart to a set. But until Python 3.15, nothing offered that immutable twin for a dict. Python 3.15 finally closes that asymmetry with a new built-in type.
PEP 814 adds frozendict to the built-in namespace as an immutable mapping type. It behaves like a dictionary whose contents are fixed once you build it, so you can read the mapping but never add, remove, or replace a key or a value. That restriction is what lets a frozen mapping be hashable, safely shared across threads, and used in places a regular dict can’t go.
Why did that take until 2026? Dictionaries have been in Python since the beginning, and the need for a frozen one isn’t exactly new.
Trace the History of Frozen Mappings
Victor Stinner first proposed a frozendict built-in back in 2012. PEP 416 laid out much the same design that eventually shipped, and Guido van Rossum rejected the PEP on the grounds that almost nobody was asking for one. The people who did use frozen mappings, he argued, treated them as a hint rather than a guarantee:
According to Raymond Hettinger, use of frozendict is low. Those that do use it tend to use it as a hint only, such as declaring global or class-level “constants”: they aren’t really immutable, since anyone can still assign to the name.
— Guido van Rossum
As things turned out, that rejection wasn’t a dead end. Guido closed the PEP by suggesting that exposing Python’s existing read-only dictionary proxy as a public type sounded worthwhile, and types.MappingProxyType arrived in Python 3.3 as a direct result. For over a decade, that proxy was the closest thing to an answer.
Seven years later, Yury Selivanov tried a different angle. PEP 603 proposed a frozenmap type for the collections module, built on a hash array mapped trie so that you could cheaply derive a modified copy. That PEP is still sitting in draft status today, never accepted and never formally rejected.
Meanwhile, the community had long since stopped waiting for the standard library to catch up. Third-party packages like frozendict and immutables filled the gap for anyone who needed an immutable mapping badly enough to add a dependency.
What finally changed the calculus was free threading. When multiple threads genuinely run Python code at the same time, a mapping that nobody can mutate stops being a stylistic preference and starts being a concurrency tool. PEP 814 landed for Python 3.15, fourteen years after the first attempt.
Compare MappingProxyType With frozendict
If you’ve reached for MappingProxyType before, then you might wonder what frozendict adds, given that the proxy already blocks writes:
>>> from types import MappingProxyType
>>> settings = {"debug": False}
>>> readonly = MappingProxyType(settings)
>>> readonly
mappingproxy({'debug': False})
>>> readonly["debug"] = True
Traceback (most recent call last):
...
TypeError: 'mappingproxy' object does not support item assignment
The catch is that a proxy isn’t a copy. It’s a live view onto a dictionary that somebody else still owns, so changes to the original show straight through:
>>> settings["debug"] = True
>>> readonly["debug"]
True
Nobody wrote through the proxy, and yet the value it reports changed. The proxy protects the object from you, not from whoever handed it to you.
The second limitation cuts deeper than the first. A proxy delegates hashing to whatever sits underneath it, so wrapping a dictionary leaves you with something you still can’t hash:
>>> hash(readonly)
Traceback (most recent call last):
...
TypeError: unhashable type: 'dict'