Dictionaries, Hashing, Lookup & Mapping Patterns
The dictionary (dict) is Python’s core data structure. Beyond providing $O(1)$ average-case key lookup, CPython’s modern dictionary implementation (PEP 468/469, designed by Raymond Hettinger) uses a compact high-density memory architecture that preserves insertion order while reducing memory usage by up to 60%.
This chapter details CPython compact dictionary architecture, hash collision resolution mechanics, SipHash anti-DoS security, and key-sharing dictionaries.
1. CPython Compact Dictionary Architecture (PEP 468/469)
Prior to Python 3.6, PyDictObject allocated a sparse array of 24-byte entry slots, wasting significant memory. Modern CPython uses a two-table compact architecture:
- Sparse Indices Table: An array of 1-byte, 2-byte, or 4-byte integers storing array indices or
-1for empty slots. - Dense Entries Array: A compact contiguous array storing
PyDictKeyEntrystructs (hash,key_ptr,value_ptr).
Modern Compact PyDictObject Memory Layout:
Sparse Indices Array (1 byte per slot for small dicts):
Slot Index: [ 0 | -1 | 2 | -1 | 1 | -1 | -1 | -1 ]
| | |
v v v
Dense Entries Array (24 bytes per entry, contiguous in memory):
Entry 0: [ hash: 0x8A3F | key: &"id" | value: &101 ]
Entry 1: [ hash: 0x4B12 | key: &"name" | value: &"Ada" ]
Entry 2: [ hash: 0x9F01 | key: &"role" | value: &"Admin" ]Why Dicts Preserve Insertion Order:
Because new key-value pairs are appended to the Dense Entries Array sequentially, iterating over a dictionary iterates through the dense array from index 0 to $N-1$, naturally preserving insertion order!
2. Collision Resolution: Perturb-based Open Addressing
When looking up a key, CPython hashes the key using hash(key). If two keys map to the same index slot (index = hash & mask), a collision occurs.
CPython resolves collisions using open addressing with perturbation:
Perturb Formula:
i = (5 * i + 1 + perturb) % mask
perturb = perturb >> 5Perturbation Probe Sequence:
Hash Key -> Initial Slot Index (i = hash & mask)
|
+---> Slot Occupied by another key? (Collision!)
|
v
Calculate Next Probe: i = (5*i + 1 + perturb) & mask
|
v (Repeats until matching key or empty slot is found)This formula shifts the top bits of the 64-bit hash into the lower index calculation on each probe, ensuring all bits of the hash participate in probing and preventing linear clustering attacks.
3. Hash Randomization (SipHash Anti-DoS Security)
If an attacker knows your application’s hash algorithm, they can craft thousands of keys that all produce identical hash values (hash1 == hash2). Sending these keys in a web request forces the dictionary into $O(N)$ linear probe chains, spiking server CPU to 100% (Hash Collision Denial of Service).
CPython mitigates this by applying SipHash-2-4 with a random per-process secret seed initialized at VM startup. The string "user_123" will produce completely different hash values across separate Python process runs.
4. Production Key Contracts & Key-Sharing Dicts
- Hashable Key Contract: A key must implement
__hash__and__eq__. The hash value must remain immutable while stored in the mapping. Never use mutable objects (lists, dicts) as keys. - Key-Sharing Dicts (
PyDictKeysObject): Object instances (__dict__) share a single key table across instances of the same class, allocating memory payload only for values.