How to Make an Immutable Dict in Python

Immutaturtle.

Python’s built-in collection types come in mutable and immutable flavours, but one is conspicuously missing:

Mutable VersionImmutable Version
listtuple
setfrozenset
dict???

Where is “frozendict”? It could be useful…

PEP 416 proposed a frozendict type for Python 3.3, back in 2012. The PEP was rejected, for several good reasons. The reasoning includes several questions about the utility of an immutable dict, which are worth checking out before you add them to your code.

But the PEP did give us a tool for emulating immutable dicts: types.MappingProxyType. This type is a read-only proxy for a dict or other mapping. Python uses this type internally for important dictionaries, which is why you can’t monkey-patch built-in types willy-nilly. The only change in Python 3.3 was to expose this type for user code.

(PEP 613 re-suggests adding an immutable dict-like type, called frozenmap. But it’s still a draft.)

How to Use MappingProxyType

To create an “immutable” dict, create a MappingProxyType from the dict, without retaining any references to the underlying dict:

from types import MappingProxyType

power_levels = MappingProxyType(
    {
        "Kevin": 9001,
        "Benny": 8000,
    }
)

You can read from the mapping proxy with all the usual methods:

In [1]: power_levels["Kevin"]
Out[1]: 9001

In [2]: power_levels["Benny"]
Out[2]: 8000

In [3]: list(power_levels.keys())
Out[3]: ['Kevin', 'Benny']

But, any attempt to change values will result in a TypeError:

In [4]: power_levels["Benny"] = 9200
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-39fccb3e4f76> in <module>
----> 1 power_levels["Benny"] = 9200

TypeError: 'mappingproxy' object does not support item assignment

Noice.

Since there are no references to the underlying dict, it cannot change. The dict isn’t accessible through any attribute on MappingProxyType either—its only reference is in an invisible C-level pointer.

If you do retain a reference to the dict in a second variable, any mutations to it will show in the proxy:

In [5]: original = {"kevin": 9001}

In [6]: proxy = MappingProxyType(original)

In [7]: proxy["kevin"]
Out[7]: 9001

In [8]: original["kevin"] = 9002

In [9]: proxy["kevin"]
Out[9]: 9002

How to Make Mutated Copies

To create a copy of the mapping proxy with changes, you can use Python 3.9’s dict merge operator. Merge the mapping proxy with a new dict, and pass the result to MappingProxyType:

In [10]: benny_better = MappingProxyType(power_levels | {"Benny": 9200})

In [11]: benny_better
Out[11]: mappingproxy({'Kevin': 9001, 'Benny': 9200})

For more complex modifications, you can copy the mapping proxy into a new dict, make changes, and then convert the result into a mapping proxy:

In [12]: new_world = power_levels | {}

In [13]: del new_world["Benny"]

In [14]: del new_world["Kevin"]

In [15]: new_world["Bock"] = 100

In [16]: new_world = MappingProxyType(new_world)

In [17]: new_world
Out[17]: mappingproxy({'Bock': 100})

Yabba-dabba-doo!

Thrid Party Packages

There are several third party packages that provide immutable data structures, such as immutables and pyrsistent. These have slightly nicer APIs, but come with different tradeoffs such as performance and maintenance status. You may want to research them if MappingProxyType doesn’t work for you, but I’d encourage using the standard library as much as possible.

Fin

May your data only mutate when you want it to,

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: