1.2.24 · D5Introduction to Programming (Python)

Question bank — Dictionaries — key-value pairs, access, methods (keys, values, items, get, update)

1,815 words8 min readBack to topic

Picture the machinery first

The traps below all trace back to one picture: a dictionary is an array of buckets, and the key's hash() chooses the bucket. Study these three figures once, and most answers become obvious.

1 — How a key finds its bucket. The key is run through hash(), that number is squeezed into a bucket index, and the value is filed there. Reading is the same jump — one hop, not a scan. This is the whole reason lookup is fast.

Figure — Dictionaries — key-value pairs, access, methods (keys, values, items, get, update)

2 — Collisions and the worst case. Two different keys can land in the same bucket (a collision). Python then stores both in that bucket and, on lookup, uses the key's == (__eq__) test to tell them apart. If every key collided into one bucket, lookup would degrade to a linear scan — average , worst case . This is why a key needs both a good __hash__ (spreads keys out) and a correct __eq__ (resolves the ties).

Figure — Dictionaries — key-value pairs, access, methods (keys, values, items, get, update)

3 — Why a view is "dynamic". keys()/values()/items() don't copy anything — they're a window aimed at the live bucket array. Insert, delete, or clear() the dict, and the same view shows the change immediately.

Figure — Dictionaries — key-value pairs, access, methods (keys, values, items, get, update)

True or false — justify

A dictionary can contain two identical keys.
False — keys are unique. Writing {"a":1,"a":2} doesn't error; the second value silently overwrites the first, leaving {"a":2}.
d.get("x") and d["x"] behave the same when the key exists.
True — both return the stored value. They only diverge on a missing key: get returns None, brackets raise KeyError.
Dictionaries have been unordered forever in Python.
False — since Python 3.7 they preserve insertion order as a language guarantee, so iteration follows the order you added pairs.
A tuple can always be used as a dictionary key.
False — only if every element inside it is also hashable. (1,2) works, but (1,[2]) raises TypeError because the inner list is unhashable.
d.keys() returns a list of the keys.
False — it returns a dynamic view (figure 3), not a list. The view updates live as the dict changes and must be wrapped in list() to become an actual list.
Two different keys can map to the same value.
True — uniqueness is required for keys, not values. {"a":1,"b":1} is perfectly legal.
Adding a key that already exists creates a second entry.
False — assignment to an existing key overwrites its value in place; the count of pairs stays the same.
len(d) counts the total number of keys plus values.
False — it counts pairs (equivalently, keys). A dict with 3 pairs has len 3, not 6.
Iterating a dict with for x in d gives you the values.
False — plain iteration yields keys. Use d.values() for values or d.items() for both.
d.values() guarantees the values have no duplicates.
False — only keys are unique; values may repeat freely, so the values view can contain duplicates.
Dict lookup is always , no matter what.
False — it is on average. If many keys collide into the same bucket (figure 2), lookup walks that bucket's chain, degrading toward in the worst case.

Spot the error

d = {[1,2]: "pair"}
A list is mutable and therefore unhashable, so it cannot be a key. This raises TypeError: unhashable type: 'list'. Use a tuple (1,2) instead.
for k, v in d: print(k, v)
Iterating a dict yields single keys, so unpacking into two names fails with ValueError (or unpacks the key's characters). You need d.items() to get (key, value) pairs.
total = sum(d) where d = {"a":90,"b":85}
sum(d) sums the keys (strings here → TypeError). To add the numbers use sum(d.values()).
city = d["city"] # I expect None if absent
Brackets never return None on a miss — they raise KeyError. For a None-on-miss result you must use d.get("city").
d.update("age", 20)
update doesn't take two positional arguments. Valid forms are a mapping d.update({"age":20}), an iterable of pairs d.update([("age",20)]), or keyword arguments d.update(age=20) — or simply d["age"] = 20.
del d["x"] when "x" may not exist
del on a missing key raises KeyError. Guard with if "x" in d: first, or use d.pop("x", None) which won't crash.
keys = d.keys(); keys.append("z")
A keys view has no .append and cannot be edited — it mirrors the dict, it doesn't own the data. Add via the dict itself: d["z"] = ....
if d.get("x") == None: ... and treating a real stored None as "missing"
If a key legitimately maps to None, .get() returns None for both "absent" and "present-but-None" — indistinguishable. Use "x" in d when you must tell them apart.

Why questions

Why must dictionary keys be hashable?
The value's bucket is computed from hash(key) (figure 1). If a key's hash could change (like a mutable list), Python could no longer find the bucket it filed the value into, so only stable-hash (immutable) keys are allowed.
Why is key lookup in a dict average , while searching a list for a value is ?
A dict hashes the key straight to a bucket and reads once, regardless of size. Note: indexing a list by position (lst[3]) is also — it's searching a list for a matching value (x in lst / lst.index(x)) that costs because it scans element by element.
Why does a good key need both __hash__ and __eq__?
__hash__ picks the bucket (spreading keys out so most buckets hold one item); __eq__ resolves collisions inside a bucket by confirming which stored key truly matches. Without a correct __eq__, colliding keys couldn't be told apart.
Why does .get() exist when brackets already read values?
.get() lets a program continue when a key might be absent, returning None or a chosen default instead of crashing — the common real-world case of "look, but it might not be there."
Why are keys()/values()/items() called views rather than copies?
They are lightweight windows onto the live dict (figure 3), so creating them is cheap () and they automatically reflect later changes — a copy would cost memory and go stale.
Why can a tuple be a key but a list cannot?
Tuples are immutable, so their hash is fixed for life; lists are mutable, so their contents (and hash) could change after insertion, breaking the lookup contract.
Why does d["age"] = 20 then d["age"] = 21 leave only one entry?
Each key is unique, so the second assignment targets the same bucket and overwrites — creation happens only the first time a key appears.

Edge cases

What does {} mean — an empty dict or empty set?
{} is an empty dict. An empty set must be written set(), because the curly-brace default belongs to dictionaries.
What happens if you use True and 1 as separate keys?
They collide and are equalTrue == 1 and hash(True) == hash(1) — so {True: "a", 1: "b"} becomes {True: "b"} with one entry; the later value wins and the key keeps its first form.
Is d.pop("x") safe if "x" is missing?
Not by default — it raises KeyError. Supply a fallback like d.pop("x", None) to make it safe.
What does iterating an empty dict do?
Nothing crashes; the loop body simply never runs because there are zero keys to yield — a clean degenerate case.
Can a dict be a value inside another dict?
Yes — values have no restrictions, so nested dicts (dicts of dicts) are common. Only keys face the hashable requirement.
Can a dict be a key inside another dict?
No — dicts are mutable and unhashable, so they cannot serve as keys, only as values. Use a frozenset or tuple if you need a hashable stand-in.
If you modify a dict while looping over d.items(), what happens?
Adding or deleting keys mid-iteration raises RuntimeError: dictionary changed size during iteration. Loop over list(d.items()) if you must mutate.
What is the worst-case lookup cost if every key hashes to one bucket?
— the "hash jump" lands everyone in the same bucket (figure 2), so Python must walk the chain and compare with ==, exactly like a list scan.

Recall One-line survival summary

Keys: unique + hashable, needing both __hash__ (bucket) and __eq__ (tie-break). Brackets bang, .get() is gentle. Views are live windows, not copies. Lookup is average / worst. Assignment overwrites, never duplicates. Iteration hands you keys. {} is a dict, set() is the empty set.

Connections

  • Hinglish version
  • Hashing and Hash Tables
  • Tuples — immutable sequences
  • Sets — unique hashable elements
  • Lists — ordered mutable sequences
  • For loops and iteration
  • Big-O Notation — time complexity