Skip to main content

🧊 FrozenDict

frozendict logo

Crates.io Docs.rs PyPI npm License: MIT CI

frozendict is a state of the art world's most memory-efficient immutable hashmap written in 100% safe Rust, with native Python and Node.js bindings 🗿.

frozendict banner

🚀 Installation

Platform Command
Rust library cargo add frozendict
Python pip install frozndict
Node.js npm i frozendict
Debian/Ubuntu Download .deb from GitHub Releases
RHEL/Fedora Download .rpm from GitHub Releases

🤔 What does this crate provide?

frozendict provides a fully immutable, hashable dictionary for Python and Node.js backed by a high-performance Rust core. It:

  • Stores entries in a single contiguous sorted heap allocation, zero per-entry heap overhead.
  • Looks up keys in O(log n) via binary search, no hashing, no pointer-chasing, excellent cache behaviour.
  • Caches its hash at construction, repeated hash() calls are O(1).
  • Integrates with Python's dict protocol (keys(), values(), items(), pickle, copy, | merge operator).
  • Exposes a native Node.js FrozenDict class with TypeScript declarations via napi-rs.
  • Provides functional update primitives: merge, with, without, intersection, union, difference.
Property FrozenMap (this crate) HashMap / BTreeMap
Memory overhead Zero: exactly n × entry_size 1.5-2× allocator overhead
Allocation count One at construction One per entry (BTreeMap)
Lookup O(log n) binary search O(1) / O(log n)
Cache behaviour Excellent: sequential prefetch Pointer-chasing on tree nodes
Mutation Impossible by design &mut self methods exist
Hash stability O(1): pre-computed at init O(n) on every hash() call

Binary search beats hash-map lookup for maps with fewer than ~64 entries because it avoids hashing and pointer-chasing. For larger maps the memory savings (up to 2×) and cache locality more than compensate. See Cache-oblivious binary search and the AHash paper for background.

🦀 Rust

[dependencies]
frozendict = "2.1.1"
use frozendict::frozen_map::FrozenMap;

let map: FrozenMap<&str, i32> = FrozenMap::new([("b", 2), ("a", 1)]);
assert_eq!(map["a"], 1);
assert_eq!(map.len(), 2);

let updated = map.with("c", 3);
assert_eq!(updated.len(), 3);

let merged = updated.merge(&FrozenMap::new([("a", 99)]));
assert_eq!(merged["a"], 99);

For the full API reference see RUST.md.

🐍 Python

pip install frozndict
from frozndict import FrozenDict

d = FrozenDict({"a": 1, "b": 2})
print(d["a"])          # 1
print(hash(d))         # stable integer (pre-computed at construction)

d2 = d | {"c": 3}      # returns a new FrozenDict
print(d2.keys())

For full docs see PYTHON.md.

🟩 Node.js

npm install frozendict
const { frozenDict, FrozenDict } = require("frozendict");

const d = frozenDict({ a: 1, b: "hello", c: [1, 2, 3] });
console.log(d.get("a")); // 1
console.log(d.size); // 3
console.log(d.has("z")); // false

const d2 = d.merge({ d: true });
console.log(d2.size); // 4
console.log(d2.toJSON()); // '{"a":1,"b":"hello","c":[1,2,3],"d":true}'

For full docs see NODE.md.

🔭 Features

Feature Default Description
python ❌ Python extension module via PyO3/maturin
node ❌ Node.js native add-on via napi-rs

📊 Benchmarks

🚀 Performance Highlights

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 per operation (smaller is better), measured on x86-64 Linux with LTO=fat, opt-level=3, codegen-units=1, panic=abort, target-cpu=native.

Operation (1000 items) Python dict frozendict (C) immutables.Map frozndict 🧊
Construction 6.41 µs 🏆 7.73 µs 244.91 µs 98.72 µs
Clone O(1) 6.37 µs 69.13 ns 🏆 411.89 ns 117.24 ns
Equality 19.18 µs 19.61 µs 24.39 ns 🏆 34.92 ns
Iteration 7.10 µs 7.19 µs 14.89 µs 4.13 µs 🏆
copy() 6.45 µs 323.22 ns 310.04 µs 63.25 ns 🏆
hash() N/A 168.11 ns 45.38 ns 45.25 ns 🏆
Lookup 33.33 ns 🏆 56.03 ns 47.48 ns 82.98 ns

Benchmarked with timeit (min of 7 runs × 2 000 iterations). Python 3.12.

🏆 Fastest Iteration in Class

Keys are stored in a single contiguous sorted Box<[K]>, sequential iteration has perfect prefetch behaviour. At 1000 elements, frozndict iterates 1.9× faster than frozendict (C) and 3.5× faster than immutables.Map.

🏆 Smallest copy() Overhead

copy() and __deepcopy__() share the backing Arc<FrozenDictInner>, O(1), just an atomic reference count increment. At 64 ns, frozndict is 5× faster than frozendict (C) on copy.

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 (0x9e3779b97f4a7c15, 0x517cc1b727220a95). Subsequent hash() calls are a single field read, O(1), allocating nothing.

O(1) Clone & Identity Equality

A shared Arc<FrozenDictInner> is re-used across the original, all copies, and frozendict(existing_fd) constructors. No data is ever duplicated. __eq__ short-circuits in O(1) via Arc::ptr_eq before comparing pre-computed hashes.

Smallest Memory Footprint

Entries sit in a SoA layout: a Box<[K]> for keys and a Box<[V]> for values, two contiguous allocations. Binary search only touches the key array: 2× smaller cache footprint compared to AoS layouts on large value types.

Infallible Rust-Level Immutability

Mutation is blocked at the Rust binary level. There are no &mut self methods, not just descriptor tricks.

Rust micro-benchmarks (cargo bench)

Benchmark Time
construction/4 ~105 ns
construction/64 ~821 ns
construction/1024 ~16.1 µs
construction/65536 ~1.32 ms
lookup/hit/4 ~5.94 ns
lookup/hit/64 ~5.83 ns
lookup/hit/1024 ~5.87 ns
lookup/hit/65536 ~5.83 ns
lookup/miss/64 ~6.48 ns
lookup/miss/65536 ~6.51 ns
iteration/keys/65536 ~20.8 µs
iteration/values/65536 ~20.8 µs
iteration/items/65536 ~56.2 µs
hash/precomputed/65536 ~13.1 ns
equality/equal/16384 ~4.82 µs
equality/unequal/16384 ~969 ps
functional/merge/256 ~7.31 µs
functional/with/4096 ~64.6 µs
functional/without/4096 ~79.7 µs
fromkeys/4096 ~56.9 µs
string_keys/lookup/hit_1024 ~12.9 ns
string_keys/lookup/miss_1024 ~5.58 ns

Run benchmarks yourself:

cargo bench                              # Rust
pip install frozndict frozendict immutables
python benchmarks/bench_compare.py      # Python comparison

🔒 Safety

This crate uses #![forbid(unsafe_code)] in all Rust modules except the Node.js FFI layer, which requires unsafe for napi-rs interop. Every other byte of implementation is safe Rust.

📚 Further Reading

📄 License

Licensed under the MIT License.

Release files for frozndict 2.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for frozndict 2.1.1
File Interpreter ABI Platform
frozndict-2.1.1-cp312-cp312-manylinux_2_34_x86_64.whl CPython 3.12 CPython 3.12 Linux glibc 2.34+ x86-64 Details

Release files / frozndict-2.1.1-cp312-cp312-manylinux_2_34_x86_64.whl

Download URL frozndict-2.1.1-cp312-cp312-manylinux_2_34_x86_64.whl
Size 259.7 kB
Tags CPython 3.12 Linux glibc 2.34+ x86-64
SHA-256 checksum
How to use checksums
151487ad7930cc80d3425cf08ec61627362e68b9d08b4ec04d8e9029fec0054b
BLAKE2b-256 checksum
How to use checksums
99e78633aeb9ae15ec4848f4041870a3299f8d6d1c658bd21d266941de5dce96
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

2.1.1 This release

1 release file

2.1.0

1 release file

2.0.0

1 release file

1.0.11

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page