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:

  1. Sparse Indices Table: An array of 1-byte, 2-byte, or 4-byte integers storing array indices or -1 for empty slots.
  2. Dense Entries Array: A compact contiguous array storing PyDictKeyEntry structs (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 >> 5
Perturbation 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.
Display Options
Appearance
Text Size
100%