How to Make an Immutable Dict in Python

Python’s built-in collection types come in mutable and immutable flavours, but one is conspicuously missing:
Mutable Version | Immutable Version |
---|---|
list | tuple |
set | frozenset |
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 dict
s: 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 API’s, 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.
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
Related posts:
- A Python Script Template with Sub-commands (and Type Hints)
- How to Create a Transparent Attribute Alias in Python
- The Well-Maintained Test: 12 Questions for New Dependencies
Tags: python