immutabledict review
immutabledict 4.3.1 wraps a Python `dict` behind the complete read-only `Mapping` interface. Indexing, iteration, length, equality, hashing, union operators, and pickling work, while assignment and in-place union do not. Methods named `set`, `delete`, `discard`, and `update` copy the top-level mapping and return another immutabledict. The latest patch adjusts constructor overloads for pytype and pyrefly; version 4.3.0 also fixed `ImmutableOrderedDict`, whose backing class had silently been plain `dict`. Values are not frozen, so a list or nested dictionary inside the wrapper remains mutable.
immutabledict 4.3.1 installed in 0.2 seconds as 1 dependency-free MB and imported in 0.09 seconds in our sandbox, so it is a low-cost choice for small, typed, top-level immutable mappings. Choose a persistent map for heavy revision workloads, and never treat mutable nested values as frozen.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import immutabledict in 0.09s · pure Python · py.typed · requires Python >=3.8,<4.0 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does immutabledict install cleanly?
Yes. In a fresh container with an empty cache, pip install immutabledict finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does immutabledict need to run?
Python >=3.8,<4.0, and nothing compiled: it is pure Python. In our run import immutabledict succeeded in 0.09s, and the package ships py.typed for type checkers.
immutabledict or frozendict: which should you use?
frozendict: Use it when its mature frozen-mapping implementation fits and the LGPL-3.0 license is acceptable to your project. immutabledict 4.3.1 installed in 0.2 seconds as 1 dependency-free MB and imported in 0.09 seconds in our sandbox, so it is a low-cost choice for small, typed, top-level immutable mappings.
When should you not use immutabledict?
Nested values must also be immutable. The constructor copies the outer mapping into a regular dictionary and retains references to lists, sets, objects, and nested dictionaries.
Use it if
- A public API should return a Mapping that callers cannot change through item assignment.
- Configuration keys and values are hashable and the whole mapping needs to work as a set member or dictionary key.
- A typed Python 3.8 through 3.14 project wants a pure-Python mapping with no runtime dependencies.
- Code benefits from copy-on-write helpers for small mappings and does not need structural sharing across large versions.
- Nested values must also be immutable. The constructor copies the outer mapping into a regular dictionary and retains references to lists, sets, objects, and nested dictionaries.
- Your mapping may contain unhashable values. `hash()` combines hashes of every key-value pair and raises `TypeError` when a value such as a list cannot be hashed.
- You create many revisions of a large mapping. `set`, `delete`, and `update` copy the full backing dictionary each time; pyrsistent is designed for structural sharing instead.
- Callers expect `copy(**changes)` from older frozendict releases. Since immutabledict 3.0, `copy()` accepts no changes and mirrors the signature of `dict.copy`.
- You only need a read-only view over a dictionary that another owner may update. `types.MappingProxyType` is in the standard library and reflects changes to its underlying mapping.
Setup reality
We installed immutabledict 4.3.1 in a fresh Python 3.12 Bookworm sandbox. The install completed in 0.2 seconds and left 1 package using 1 MB on disk. It has 0 direct dependencies, is pure Python, requires Python 3.8 or newer and lower than 4.0, and ships a py.typed marker. pip-audit found 0 known vulnerabilities.
No credentials, service, native compiler, or config file is involved. import immutabledict worked in 0.09 seconds on our box. PyPI publishes a universal Python 3 wheel. Its package metadata leaves the license field empty even though the repository identifies an MIT license, so automated license policy should inspect the repository license rather than relying on that one metadata field.
Immutability stops at the first level. immutabledict({"items": []}) still exposes the same mutable list, and changing that list after a hash has been cached can break assumptions made by sets or dictionary keys. Use hashable, stable values whenever an instance will be hashed. Construction copies the input mapping, so later top-level changes to the original dictionary do not appear in the wrapper.
Every set, delete, or update call allocates and fills another top-level dictionary. delete raises KeyError for a missing key; discard returns the same object when the key is absent. The left | right result follows the left operand: an immutabledict on the left returns an immutabledict, while a plain dict on the left returns a dict. Version 4.3.1 only changes typing behavior, so runtime users do not need a migration.
Patterns
Freeze the top level of a dictionary create-mapping
from immutabledict import immutabledict
settings = immutabledict({"region": "eu", "retries": 2})
print(settings["region"])The constructor copies the top-level input dictionary. Objects stored as values keep their original identity and mutability.
Confirm callers cannot assign a key reject-assignment
settings = immutabledict({"region": "eu"})
try:
settings["region"] = "us"
except TypeError:
passThe class implements Mapping rather than MutableMapping, so item assignment has no supported method.
Return a copy with one value changed set-value
base = immutabledict({"region": "eu", "retries": 2})
changed = base.set("retries", 3)
assert base["retries"] == 2
assert changed["retries"] == 3`set` copies the complete backing dictionary. Repeated edits to a large mapping do not share structure.
Apply several changes in one copy update-values
base = immutabledict({"region": "eu", "retries": 2})
changed = base.update({"region": "us", "timeout": 5})The 4.3.1 method accepts a dictionary and returns a new immutabledict; it does not mutate `base`.
Remove a key and fail if it is absent delete-required-key
without_timeout = settings.delete("timeout")
try:
settings.delete("missing")
except KeyError:
pass`delete` mirrors strict dictionary deletion and raises `KeyError` for an unknown key.
Remove a key only when present discard-optional-key
clean = settings.discard("debug")
assert clean is settings # when debug was absentVersion 4.2 added `discard`. It returns the same instance when the key is missing, avoiding an unnecessary copy.
Merge mappings with right-hand values winning merge-right-biased
defaults = immutabledict({"region": "eu", "retries": 2})
settings = defaults | {"retries": 4, "debug": True}
assert isinstance(settings, immutabledict)With immutabledict on the left, `|` returns an immutabledict and later values replace duplicate keys.
Know when union returns a regular dictionary reverse-union
frozen = immutabledict({"retries": 2})
result = {"region": "eu"} | frozen
assert type(result) is dictA plain dict on the left calls the reverse union path, whose return type is `dict`, not immutabledict.
Use a mapping with hashable contents as a key use-as-key
query = immutabledict({"region": "eu", "page": 1})
cache = {query: "result"}
assert cache[query] == "result"Every key and value must be hashable. A list or nested dict value makes `hash(query)` raise `TypeError`.
Freeze nested data explicitly avoid-shallow-mutation
roles = ("reader", "editor")
user = immutabledict({"id": 7, "roles": roles})Use tuples, frozensets, or recursively frozen mappings for nested data. immutabledict freezes only its own outer mapping.
Build a typed mapping from key-value pairs construct-from-pairs
ports = immutabledict([("http", 80), ("https", 443)])
assert list(ports) == ["http", "https"]Version 4.3 adds typed constructor overloads for iterables and mappings; 4.3.1 corrects their behavior in pytype and pyrefly.
Round-trip through pickle pickle-mapping
import pickle
frozen = immutabledict({"region": "eu"})
restored = pickle.loads(pickle.dumps(frozen))
assert restored == frozenThe pickle payload stores the mapping data and omits the cached hash, since Python hash values can differ between interpreter runs.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| frozendict | PyPI | Use it when its mature frozen-mapping implementation fits and the LGPL-3.0 license is acceptable to your project. |
| pyrsistent | PyPI | Use its PMap when frequent updates to larger mappings need structural sharing instead of a full top-level copy. |
| frozenmap | PyPI | Use it when a C or Cython-backed frozen mapping is worth adding compiled-extension packaging concerns. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

