Dictionaries
Map keys to values: read safely, assign, and merge dictionaries.
Why dictionaries matter
When your question is "what value goes with this key?", a dictionary answers it fast. A list forces you to scan element by element to find a match, and that cost grows with the list. A dict jumps straight to the value. Real systems lean on this everywhere: counting events, caching results, indexing rows by id, grouping records, and passing named config around. Any "user id to profile" map or word-count tally is a dict.
The mental model: a hash map
A dictionary stores key: value pairs. Under the hood it is a hash map. Python runs each key through a hash function to find the slot where its value lives, so a lookup takes about the same time whether the dict holds 10 pairs or 10 million. That average O(1) lookup, insert, and delete is the reason the type exists.
Two consequences of the hash-map design:
- Keys must be hashable, which in practice means immutable.
str,int, andtuplework as keys; alistdoes not and raisesTypeError. - Since Python 3.7 a dict keeps insertion order, so iterating returns keys in the order you added them.
Reading and writing
Index a key with d[key], but a missing key raises KeyError. Reach for .get(key, default) when the key might be absent and you want a fallback instead of a crash:
prices = {"apple": 3, "pear": 2}
prices["apple"] # 3
prices.get("banana", 0) # 0, the default, because "banana" is absent
prices["plum"] = 4 # bracket assignment adds a new pair
prices["apple"] = 5 # the same syntax updates an existing key
That .get(name, 0) pattern is exactly what the lookup exercise needs.
| You write | Key is present | Key is missing |
|---|---|---|
| d[key] | the value | raises KeyError |
| d.get(key) | the value | None, silently |
| d.get(key, 0) | the value | 0, the fallback you chose |
| d.setdefault(key, 0) | the value | inserts 0 into d and returns it |
| key in d | True | False, and it tests KEYS, never values |
Merge two dicts into a brand new one with {**a, **b}. The demo below spreads {"fig": 6} into prices and leaves both originals untouched. When both sides share a key, the right-hand dict wins:
{**{"x": 1}, **{"x": 9}} # {'x': 9}, b overrides a on the shared key
That is the merge_two exercise in one line. Python 3.9+ also offers a | b for the same result.
Pitfalls
.get with no default returns None, not an error, when the key is missing, so prices.get("banana") gives None rather than 0. Always pass the fallback you actually want. Watch the direction too: key in d tests keys, not values, so "apple" in prices is True but 3 in prices is False. And bracket assignment overwrites silently, so d[key] = value replaces any existing value with no warning. That same rule is why the right operand wins in a merge.
Interview nuance: interviewers probe why dict lookup is O(1) while list membership (x in some_list) is O(n). The dict hashes the key and jumps to a slot; the list compares element by element. When a solution repeatedly asks "have I seen this before?", swapping a list for a dict or set is often the entire optimization, turning an O(n²) loop into O(n).
prices = {"apple": 3, "pear": 2}
print(prices["apple"]) # 3
print(prices.get("banana", 0)) # 0
print({**prices, **{"fig": 6}}) # {'apple': 3, 'pear': 2, 'fig': 6}Apply
Your turn
The task this lesson builds to.
Implement lookup(prices, name): return the price for name from the prices dict, or
0 if it isn't there.
For prices = {"apple": 3} and name = "banana", return 0.
2 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Implement merge_two(a, b): return a new dict with all pairs from a and b. When a key
is in both, b's value wins.
For ({"x": 1}, {"x": 9}) return {"x": 9}.
2 hints and 4 automated checks are waiting in the workspace.