A person operating a large machine labeled frozendict that encases documents in ice and sends them along a conveyor belt past a sign reading Safe and Hash, with a Python logo on the panel.

Python 3.15 Preview: frozendict

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 frozendict as a built-in type, so you never have to import anything to use it.
  • A frozendict is hashable only when all of its keys and values are hashable.
  • frozendict doesn’t inherit from dict, so your existing isinstance() 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.

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:

Language: Python
>>> 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:

Language: Python
>>> 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:

Language: Python
>>> hash(readonly)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'dict'

Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Article

Already a member? Sign-In

Locked learning resources

The full article is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Article

Already a member? Sign-In

About Bartosz Zaczyński

Bartosz is an experienced software engineer and Python educator with an M.Sc. in Applied Computer Science.

» More about Bartosz

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

What Do You Think?

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Become a Member to join the conversation.

Keep Learning

Related Topics: intermediate python