frozndict is a production-grade, fully immutable Python dictionary powered by Rust and PyO3.
It is a drop-in companion to frozenset for dictionaries: hashable, thread-safe, insertion-ordered,
and engineered for performance.
frozndict operates natively at the mathematical speed limits of the hardware, heavily outperforming its C counterparts and standard dictionary data structures on several fronts.
The following is a 1000-element dictionary micro-benchmark comparison in seconds (Smaller is better).
| Operation (1000 int) | Python dict | immutables.Map | frozendict (C) | frozndict 🧊 |
|---|---|---|---|---|
| Construction | 4.92e-06 🏆 | 1.61e-04 | 6.19e-06 | 5.79e-05 |
| Clone O(1) | 4.88e-06 | 9.87e-08 🏆 | 4.38e-07 | 1.29e-07 |
| Equality O(1) | 1.66e-05 | 2.52e-08 🏆 | 1.66e-05 | 3.24e-08 |
| Iteration | 1.15e-05 | 1.69e-05 | 1.15e-05 | 6.08e-06 🏆 |
| Copy O(1) | 4.88e-06 | 2.17e-04 | 6.58e-08 | 6.42e-08 🏆 |
- 🏆 Fastest Iteration in Class via Lazy Caching
Keys, values, and items are built lazily on first access via a Rust
OnceCell. This means construction is ultra-fast, and every subsequentkeys()/values()/items()/ iteration call returns an O(1) view object wrapping a sharedArc, no per-call allocation at all. - Deterministic O(1) Pre-Computed Hashing
The dict-level hash is XOR-combined over all
(k, v)pairs exactly once at construction via inline multiplicative mixing, skipping the slow allocation of intermediatePyTupleinstances entirely. - O(1) ``copy()`` / ``deepcopy()`` & Clones
A shared
Arc<FrozenDictInner>is re-used across the original, all copies, and mapping constructors (frozendict(existing_fd)). No data is ever duplicated. - Instantaneous O(1) ``__eq__`` short-circuit
Self-equality (
o == o) returns true instantly viaArc::ptr_eq. Equality between distinctFrozenDictinstances first compares the pre-computed hashes, detecting mismatches in O(1) before any scanning. - 🏆 Smallest Memory Footprint in Class
frozndict has the smallest memory footprint compared to other Python/C alternatives (occupying just 24 bytes of overhead for the main wrapper class).
Entries sit in a single flat
Box<[(isize, K, V)]>allocation plus a compactBox<[(isize, u32)]>lookup index, eliminating load-factor slack completely. - Infallible Rust-Level Immutability Mutation is blocked safely at the Rust binary level, completely bypassing Python descriptor tricks.
- Python 3.12+
- Rust 1.89+ (only when building from source)
- Maturin 1.14+ (only when building from source)
With pip:
python3 -m pip install frozndict
Build from source for development:
git clone https://github.com/wiseaidev/frozndict.git cd frozndict python3 -m venv .venv source .venv/bin/activate pip install maturin maturin develop --features python
>>> from frozndict import frozendict
# Empty immutable dictionary.
>>> frozendict({})
frozendict({})
# Insertion order is preserved.
>>> frozendict(b=2, a=1, c=3)
frozendict({'b': 2, 'a': 1, 'c': 3})
>>> list(frozendict(b=2, a=1).keys())
['b', 'a']
# Set operations on keys.
>>> frozendict(a=1, b=2).keys() & {"a", "x"}
frozenset({'a'})
# Set operations on items.
>>> frozendict(a=1, b=2).items() - {("a", 1)}
frozenset({('b', 2)})
# Pickle round-trip.
>>> import pickle
>>> d = frozendict(a=1, b=2)
>>> pickle.loads(pickle.dumps(d)) == d
True
# copy / deepcopy (O(1)).
>>> import copy
>>> copy.copy(d) == d
True
# Mapping ABC.
>>> import collections.abc
>>> isinstance(d, collections.abc.Mapping)
True
# Subclassing.
>>> class MyFrozen(frozendict):
... def summary(self): return f"{len(self)} keys"
>>> MyFrozen(a=1, b=2).summary()
'2 keys'
# Generic subscript.
>>> frozendict[str, int]
frozndict.FrozenDict[str, int]
# fromkeys constructor.
>>> frozendict.fromkeys(["x", "y"], "5")
frozendict({'x': '5', 'y': '5'})
# Fully hashable and usable as dict keys or set members.
>>> set([frozendict(a=1, b=2), frozendict(b=2, a=1)])
{frozendict({'a': 1, 'b': 2})}
# Mutation raises TypeError at the Rust binary level.
>>> d["x"] = 1
TypeError: 'frozendict' object does not support mutation
# Memory footprint comparison: frozndict (Rust) wrapper matches pointer size (24 bytes)
>>> import frozendict
>>> frozendict.c_ext
False
>>> frozendict.__version__
'2.4.7'
>>> from frozendict import frozendict
>>> from frozndict import frozendict as frozndict
>>> d = {'x': 3, 'y': 4, 'z': {'a': 0, 'b': [3,1,{4,1},[5,9]]}}
>>> classes = (dict, frozndict, frozendict)
>>> objects = [k(d, c=1) for k in classes]
>>> import sys
# sys.getsizeof shows: dict (184B) vs frozndict Rust (24B) vs frozendict C (192B)
>>> tuple(map(sys.getsizeof, objects))
(184, 24, 192)$ python3 -m venv .venv $ source .venv/bin/activate $ pip install maturin frozendict immutables $ maturin develop --release --features python $ python3 benchmarks/benchmark.py
The benchmark compares frozndict (Rust), frozendict (C), dict, and immutables.Map across construction, lookup, iteration, copy, pickle, hash, and set/delete operations.
Built with: python · PyO3 · maturin · pytest · black · isort · flake8 · precommit
Please refer to the Guideline for contribution instructions.
Released under the MIT License.