Visual walkthrough — Dictionaries — key-value pairs, access, methods (keys, values, items, get, update)
We assume only that you know a list — a row of boxes numbered 0, 1, 2, … where box number i holds one item. Everything else we build here.
Step 1 — The problem: pairs stored in two lists
WHAT. We want to remember pairs like "Asha" → 88. With only lists, the honest way is two lists lined up side by side: one for the keys (the labels we look things up by) and one for the values (what we get back).
WHY. Before we can appreciate the dictionary's trick, we must feel the pain it removes. So we start with the most basic tool — lists — and no magic at all.
PICTURE. Two rows of boxes. The top row holds keys, the bottom row holds values. Box i on top pairs with box i on the bottom.

keys = ["Asha", "Ravi", "Meera"]
values = [ 88, 91, 75 ]Here keys[0] is "Asha" and its partner values[0] is 88. The index 0 is the invisible thread linking them.
Step 2 — Looking up the slow way (scan every box)
WHAT. To find Meera's mark, we walk the key list box by box, comparing each key to "Meera". When it matches at index i, we return values[i].
WHY. This is the only thing lists let us do — there is no way to know where "Meera" lives without checking. This scanning is exactly what a dictionary abolishes.
PICTURE. A finger points at box 0, then box 1, then box 2, testing each. Only at box 2 does the arrow turn green ("match!") and drop down to grab 75.

def lookup(target):
for i in range(len(keys)): # i = 0, then 1, then 2, ...
if keys[i] == target: # compare THIS key to target
return values[i] # partner value at same indextarget— the key we are searching for.i— the box number we are currently checking.keys[i] == target— the comparison; the whole cost lives here.
Term by term: with pairs, the worst case checks all boxes. That linear growth is the enemy.
Step 3 — The wish: a formula that tells us the box
WHAT. We wish for a machine that reads a key and computes its box number directly — no walking. Call this machine , the hash function:
- — the label going in (e.g.
"Ravi"). - — a machine that scrambles the key into a number.
- — the box number that comes out.
WHY. A scan is slow because it asks every box. If a formula names the exact box in one shot, the scan disappears — one computation instead of comparisons.
PICTURE. A key drops into a funnel labelled ; a single number falls out and points at exactly one box in a row of slots.

Step 4 — Building one storage row (the hash table)
WHAT. We make one row of empty slots, size . To store a pair, compute and drop the value into slot . To read it back, compute the same and look.
WHY. Since can spit out huge numbers, we fold it into our row of slots with the remainder operator ("what's left after dividing"). This guarantees is always a legal slot number .
PICTURE. Three keys funnel through , each , each landing in its own slot. Storing and reading use the same arrow — that symmetry is why reading is instant.

- — the raw scrambled number.
- — the remainder after dividing by ; squeezes any number into .
- — the final slot, always in range.
This is the payoff: storing and reading are now one computation each — that is average , independent of how many pairs exist.
Step 5 — Why keys must be unchanging (hashable)
WHAT. Suppose we store a value under a key, then the key itself changes. Next time we compute , we get a different slot — and find nothing. The value is "lost."
WHY. The whole system rests on giving the same slot forever. A key that can mutate breaks that promise. So Python only allows immutable keys: str, int, tuple.
PICTURE. Left: a stable key (1,2) always hashes to slot 2 — read succeeds. Right: a list key [1,2] mutates to [9,2], hashes to slot 4 — read points at an empty slot.

d = {(1, 2): "ok"} # tuple key — fine, never changes
# d = {[1, 2]: "no"} # list key — TypeErrorStep 6 — Edge case: two keys land in the same slot (collision)
WHAT. Nothing stops and from giving the same slot. That clash is a collision. If we blindly overwrote, we'd lose a pair.
WHY. We must cover this — otherwise a reader hits a case we never showed. Python's fix: each slot holds a tiny list of pairs; on a clash we store both, then do a very short scan inside that one slot to pick the right key.
PICTURE. Two keys arrow into slot 1. The slot fattens into a mini-list holding both (key, value) pairs; a short local scan finds the matching key.

Step 7 — Degenerate cases: empty dict and a missing key
WHAT. Two boundary situations. (a) The dict is empty {} — nothing to compute against. (b) We compute , jump to the slot, and it's empty (or holds only other keys). The key isn't there.
WHY. Real code meets empty dicts and absent keys constantly. Bracket access d[k] treats "empty slot" as an error and crashes (KeyError); .get(k, default) treats it as "just return the fallback."
PICTURE. Left branch: d["city"] lands on an empty slot → red KeyError. Right branch: d.get("city", "NA") lands on the same empty slot → calm green "NA".

d = {"name": "Asha"} # only one pair
d["city"] # slot empty -> KeyError (crash)
d.get("city") # slot empty -> None (calm)
d.get("city", "NA") # slot empty -> "NA" (fallback)d["city"]— demands the slot be full; empty → raises.d.get("city", "NA")— inspects the slot; empty → hands back"NA".
Recall Check yourself on the boundaries
On empty {}, what does d.get("x", 0) return? ::: 0 — the default, no crash.
On empty {}, what does d["x"] do? ::: Raises KeyError.
After a collision, is lookup still fast? ::: Yes — average ; only a tiny in-slot scan.
The one-picture summary
WHAT. The whole journey compressed: a key enters , becomes a slot number , and lands in one slot of the table — replacing the slow left-hand scan with a single right-hand jump.

This single arrow — key → → slot → value — is the entire reason dictionaries exist. Everything else (iterating with .items(), .get() defaults, .update() merges) sits on top of this one instant jump.
Recall Feynman retelling of the whole walkthrough
First we tried remembering pairs with two lists lined up — one for names, one for marks. To find Meera we had to walk down the whole row asking "is this her? is this her?" — slow, and slower the more people we add (that's ).
Then we wished for a machine that, given a name, just tells us the box number. That machine is the hash function . We built one storage row of slots and said: put each value in slot — the just squeezes the big scrambled number into a real slot. Now storing and finding are each one computation — no walking. That's the instant magic.
But it only works if a name always scrambles to the same slot, so keys must never change — that's why lists (which can change) are banned and tuples (which can't) are allowed. Sometimes two names hash to the same slot (a collision); we just keep a tiny list there and take a quick peek — still fast. And if we ask for a name that was never stored, the slot is empty: brackets d[k] panic with a KeyError, while the polite .get(k, default) shrugs and hands back a fallback. That single picture — name → funnel → slot → value — is the whole idea.
Connections
- Parent topic (Hinglish)
- Hashing and Hash Tables — the machine in full
- Big-O Notation — time complexity — why beats
- Lists — ordered mutable sequences — the slow starting point
- Tuples — immutable sequences — valid (hashable) keys
- Sets — unique hashable elements — same hash-table machinery
- For loops and iteration — walking a dict's views