3.3.9 · D1Hashing

Foundations — Python dict and set internals

2,167 words10 min readBack to topic

This page is the ground floor. The parent note (Python dict and set internals) throws around hash(k), h & (m-1), perturb, , slots, dummies, powers of two, &, and %. If any of those made you pause, this is where each one is built from nothing. Read top to bottom; every symbol is earned before it is used.


1. An array and an index — the absolute starting point

Before anything hash-related, you must be comfortable with an array and an index.

  • Plain words: "give me box number " is instant — the computer computes the box's memory address by simple arithmetic (start + i × box_size) and reads it. No scanning.
  • The picture: a numbered strip of lockers (figure above). The label under each locker is its index.
  • Why the topic needs it: the entire point of a hash table is to convert a key into one of these index numbers, so we get that instant "box number " jump. Everything downstream is about which index to pick.

We will call the number of boxes (for "size of the table") throughout.


2. hash(key) — turning a thing into a number

  • Plain words: it is a "name-to-number machine." You feed it "cat", it spits out something like -4038109 (some big, seemingly random integer).
  • The picture: a black box with a key going in and a number coming out — see figure below.
  • Why the topic needs it: an index has to be a number between and . A key might be a string or a tuple. hash is the bridge: object → integer. (The integer is not yet a valid index — it's usually far too big or negative. Section 5 fixes that.)

Why this law? If equal keys could hash differently, they'd point at different boxes, and the table would store the same logical key twice — chaos. This law is exactly why 1, 1.0, and True collapse into one dict entry in the parent note: they are ==, so they are forced to hash the same. See Hash Functions for how these numbers are actually produced, and Immutability and Hashability for which objects are even allowed through the machine.


3. Bits and binary — because the index trick reads bits directly

CPython does not compute the index with ordinary division. It reads the bits of the hash. So we need to know what bits are.

For example in binary:

  • Plain words: binary is just counting with only two digits instead of ten. The rightmost bit is the "ones," next is "twos," next "fours," and so on.
  • The picture: the figure shows as lit/unlit switches, each labelled with its place value.
  • Why the topic needs it: the index formula plucks out the low bits of the hash. To understand that, you must see a number as its bits. The "low bits" are the ones on the right (small place values).

4. Powers of two and — the magic mask

Here is the key observation. Take . Then: In binary, is all s in exactly the low bits (three s here, because ).

  • Plain words: subtracting from a power of two turns it into a solid block of low s.
  • The picture: , and — the single high bit drops and the three below it all light up (shown in the next figure).
  • Why the topic needs it: this block of s is a mask. Combined with the & operation (next section), it slices off exactly the low bits of any number — which is what turns a giant hash into a legal index. See Load Factor and Rehashing for why grows by doubling (staying a power of two).

Now the punchline that the parent note states without proof:

WHAT the picture shows / WHY it works: the mask has s only in the low bits. Doing h & (m-1):

  • every high bit of meets a in the mask → forced to (thrown away),
  • every low bit of meets a in the mask → kept unchanged.

So you keep exactly the low bits — which is the remainder when dividing by , because each discarded high bit was a multiple of (i.e. a multiple of , so it vanishes under % m).

Worked check (, ):

Why prefer & over %? Same answer, but masking is a single fast CPU operation; division is slower. That's the only reason. This produces , the first slot to try — the starting index in the parent's Step 1.


6. perturb, high bits, and >>= — spreading collisions apart

Step 5 only used the low bits. So two keys with the same low bits (like the parent's and , both ending in ) start at the same slot — a collision. We need a way to send them to different later slots. That is what perturb is for.

  • Plain words: the high bits of the hash were thrown away by the mask in Step 5. perturb is a reservoir of those thrown-away bits, and we drip them back in one probe at a time.
  • Why the topic needs it: two keys sharing low bits differ in their high bits. Feeding those high bits into the probe formula makes their probe paths diverge quickly, avoiding long clumps (clustering). See Collision Resolution for the full probe machinery and Java HashMap Internals for a completely different (chaining) approach to the same problem.

You do not need to memorise the probe formula here — just know that perturb is "the high bits, saved for later," and >>= is how they're released.


7. , fill, m, n — the fullness dial

  • Plain words: is "how full the table is," between (empty) and (packed).
  • The picture: a fuel gauge. CPython keeps it below the mark; cross it and the table resizes.
  • Why the topic needs it: the average number of probes for a lookup grows like (parent note). Keeping keeps that constant around — that is why lookups are O(1) on average. We write for active keys only; fill may exceed because deleted "dummy" slots also count. See Load Factor and Rehashing and Amortized Analysis for why the occasional expensive resize still averages out to O(1) per operation.

The prerequisite map

Array and index 0..m-1

Turn key into an index

hash of key gives an integer

Binary and bits

Power of two and m minus 1 mask

Bitwise AND extracts low bits

Starting slot i0

Right shift releases high bits

perturb spreads collisions

Probe sequence

Load factor alpha equals fill over m

Keep alpha below two thirds

Resize keeps lookups O of 1


Equipment checklist

Cover the right side and test yourself. If any answer is fuzzy, reread that section.

What is an index, and what range does it live in?
The position number of a box in the array, from to .
What does hash(key) return?
A single integer (possibly huge or negative), the same one every time for equal keys within a run.
State the one law every hash must obey.
If then — equal keys hash equally.
Does equal hash imply equal keys?
No. Different keys can share a hash; that is a collision.
Write in binary and give its low 3 bits.
; low 3 bits are .
What is in binary when ?
— a block of three low s (the mask).
What does a & b do bit-by-bit?
Outputs only where both bits are , else .
Why does h & (m-1) equal h % m for power-of-two ?
The mask keeps the low bits and zeros the high bits; discarded high bits are multiples of , so they vanish under mod.
Why use & instead of %?
Same result but a single fast CPU op instead of slower division.
What is stored in perturb and how is it released?
The full hash (its high bits); perturb >>= 5 shifts them into view one probe at a time.
Why do we need perturb at all?
Keys sharing low bits collide at ; feeding in their differing high bits scatters their probe paths, avoiding clustering.
Define the load factor .
, where fill counts active entries plus deleted dummy slots.
Why does CPython cap ?
To keep the average probe count , so lookups stay O(1) on average.

Next: build the probe sequence and deletion machinery on top of these foundations — every symbol here is now in your toolbox.