immutabledict
immutabledict is a pure-Python Mapping that copies dictionary input and blocks item assignment, deletion, and in-place union. It supports normal read operations, hashing when every key and value is hashable, typing through a py.typed marker, ordered variants, and copy-on-write helpers named set, update, delete, and discard. It exists as a small MIT-licensed fork of the original frozendict project and is useful when a mapping itself must be a stable value or dictionary key.
A sound tiny wrapper for small, shallow mappings that are built once and read often. Do not mistake it for recursive freezing or an efficient persistent map; repeated updates and mutable nested values are the line where another design wins.
Use it if
- You need a hashable mapping for cache keys, sets, graph nodes, or memoization and all contained values are hashable
- A read-only Mapping API plus explicit copy-on-write set, update, delete, and discard operations fits your code
- You want a pure-Python MIT-licensed mapping with no runtime dependencies and published typing information
- You need an ImmutableOrderedDict for code that explicitly depends on OrderedDict behavior
- You need deep immutability: the constructor copies only the outer dictionary, so a list, dict, or mutable object stored as a value can still change underneath it
- You update large maps frequently: set, delete, update, union, and copy all allocate and copy a full ordinary dict rather than sharing structure
- Values may be unhashable: the hash implementation hashes every key-value pair, so a nested list or dict makes the whole mapping unusable as a key even though construction succeeds
- You only need a live read-only view: the standard library's types.MappingProxyType avoids a dependency and reflects changes to the wrapped dict, while immutabledict takes a snapshot
- You need a fixed schema or recursive persistent collections: a frozen dataclass gives named fields, while pyrsistent or immutables provides structural-sharing data structures designed for repeated updates
Setup reality
`pip install immutabledict` installs one pure-Python package with no runtime dependencies. Version 4.3.1 supports Python 3.8 through the current Python 3 line but explicitly excludes Python 4. Construction makes a shallow copy of the supplied mapping, so later top-level edits to the original dict do not appear. That does not freeze nested values: if both structures reference the same list or custom object, mutating it still changes what readers observe. Hashing is lazy and cached. Every contained key and value must therefore be hashable, and mutating a badly chosen nested custom object after the first hash can violate dictionary-key assumptions even though the wrapper itself never assigns anything. Copy-on-write helpers are convenient but not persistent data structures: source shows set, delete, update, and `|` each build a normal dict and then wrap it, making every change O(n) in map size. `copy()` returns a new object and, since 3.0, deliberately accepts no dict-style keyword changes. `left | right` takes its result type from the left side: immutabledict on the left returns immutabledict, while a normal dict on the left returns dict. `|=` always raises for an immutabledict. Equality follows Mapping behavior and does not enforce the same concrete type. Pickling is supported and deliberately omits the cached hash because Python hash values can change between interpreter runs.
Patterns
Create and read an immutable mappingcreate-readonly-mapping
from immutabledict import immutabledict
settings = immutabledict({'region': 'eu-west-1', 'retries': 3})
print(settings['region'])
print(settings.get('timeout', 30))Construction copies the outer mapping, so later top-level changes to the source dict do not affect settings.
Annotate key and value typestype-the-mapping
from immutabledict import immutabledict
ports: immutabledict[str, int] = immutabledict({'http': 80, 'https': 443})
for name, port in ports.items():
print(name, port)The package ships a py.typed marker. Value type is covariant, which helps when exposing read-only mappings through broader interfaces.
Build from a sequence of keyscreate-from-keys
from immutabledict import immutabledict
flags = immutabledict.fromkeys(['read', 'write'], False)
assert flags == {'read': False, 'write': False}As with dict.fromkeys, every key receives the same value object. Avoid one shared mutable default.
Return a copy with one value setset-copy-on-write
from immutabledict import immutabledict
base = immutabledict({'retries': 2})
updated = base.set('retries', 3)
assert base['retries'] == 2
assert updated['retries'] == 3set copies the full underlying dictionary. It is not structural sharing and gets costly for large maps with frequent changes.
Return a copy with several updatesupdate-copy-on-write
from immutabledict import immutabledict
base = immutabledict({'host': 'localhost', 'port': 8000})
production = base.update({'host': 'api.example.com', 'port': 443})update expects a dictionary and returns a new immutabledict. The original remains unchanged.
Remove a known keydelete-required-key
from immutabledict import immutabledict
record = immutabledict({'id': 7, 'debug': True})
clean = record.delete('debug')delete raises KeyError when the key is absent, matching del-style expectations while still returning a new object.
Remove a key only if presentdiscard-optional-key
from immutabledict import immutabledict
record = immutabledict({'id': 7})
unchanged = record.discard('debug')
assert unchanged is recorddiscard returns the same object when the key is missing, avoiding an unnecessary full copy.
Merge with the union operatormerge-mappings
from immutabledict import immutabledict
base = immutabledict({'color': 'blue', 'size': 'm'})
merged = base | {'size': 'l', 'stock': 4}
assert isinstance(merged, immutabledict)
assert merged['size'] == 'l'Right-hand values win. In-place union with |= raises TypeError because it would imply mutation.
Keep result type explicit in reverse unionsunderstand-reverse-union
from immutabledict import immutabledict
fixed = immutabledict({'timeout': 10})
result = {'retries': 2} | fixed
assert isinstance(result, dict)A normal dict on the left produces a mutable dict. Put immutabledict on the left or wrap the result when the output must stay immutable.
Use a mapping as a cache keyuse-as-dictionary-key
from immutabledict import immutabledict
query = immutabledict({'page': 2, 'limit': 50})
cache = {query: ['row-1', 'row-2']}
print(cache[query])Every contained key and value must be hashable. A list or nested dict value makes hash(query) raise TypeError.
Freeze nested values explicitlyavoid-shallow-mutation
from immutabledict import immutabledict
permissions = immutabledict({
'admin': frozenset({'read', 'write'}),
'viewer': frozenset({'read'}),
})
assert hash(permissions)immutabledict freezes only its own key assignments. Use immutable nested values when stable hashing or deep immutability matters.
Use the ordered variantpreserve-explicit-order
from immutabledict import ImmutableOrderedDict
steps = ImmutableOrderedDict([('build', 1), ('test', 2), ('ship', 3)])
assert list(steps) == ['build', 'test', 'ship']Modern dict already preserves insertion order. Choose this variant only when OrderedDict identity or behavior is part of the contract.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| frozendict | PyPI | You want the better-known frozen mapping package and its API or licensing fits your project |
| pyrsistent | PyPI | You need persistent maps, vectors, sets, and nested transformations with structural sharing |
| immutables | PyPI | You update large immutable mappings often and want a high-performance HAMT implementation |