Python dictionaries, explained
The dict is the workhorse of Python — it backs objects' attributes, keyword arguments,
JSON, and countless lookups. It's also a reliable interview topic because the questions
probe real understanding: why lookups are O(1), why keys must be hashable, and what changed
about ordering in Python 3.7. This guide walks through the behaviour that matters.
O(1) lookups via hashing
A dict is a hash table: it computes hash(key) to decide which bucket a value goes in,
so lookups, inserts, and deletes are average O(1) regardless of size. That's why a dict
crushes a list for "does this key exist?" — a list is O(n).
prices = {"apple": 30, "banana": 10}
prices["apple"] # O(1) — hash to the bucket, no scanning
"banana" in prices # O(1) membership test
The cost is memory (buckets and spare capacity) and that keys must be hashable — the mechanism that makes O(1) possible is the same one that constrains what can be a key.
Keys must be hashable
A key's hash must stay constant for its lifetime, so keys must be immutable/hashable: strings, numbers, tuples, frozensets — but not lists, dicts, or sets.
{(1, 2): "point"} # tuple key — fine
{[1, 2]: "point"} # TypeError: unhashable type: 'list'
If a mutable key could change after insertion, its hash would change and the value would be unfindable in its original bucket. Use a tuple instead of a list, a frozenset instead of a set.
Insertion ordering (since 3.7)
As of Python 3.7, dicts preserve insertion order as a language guarantee (it was a CPython implementation detail in 3.6). Iterating a dict yields keys in the order they were added.
d = {}
d["z"] = 1
d["a"] = 2
list(d) # ['z', 'a'] — insertion order, not sorted
This is why collections.OrderedDict is rarely needed now — though it still has uses
(order-sensitive equality and move_to_end). To sort, do it explicitly:
dict(sorted(d.items())).
get vs and setdefault
Indexing a missing key raises KeyError. dict.get(key, default) returns a default
instead, and setdefault(key, default) returns the existing value or inserts and returns
the default — handy for accumulating.
counts = {}
for word in ["a", "b", "a"]:
counts[word] = counts.get(word, 0) + 1 # no KeyError on first sight
counts # {'a': 2, 'b': 1}
groups = {}
for name in ["ann", "amy", "bob"]:
groups.setdefault(name[0], []).append(name) # insert [] if missing, then append
groups # {'a': ['ann', 'amy'], 'b': ['bob']}
For counting and grouping, collections.Counter and collections.defaultdict are cleaner
still — but get/setdefault are the built-in tools to know.
Merging dictionaries
Since Python 3.9 the | operator merges dicts; |= updates in place. Before that,
{**a, **b} unpacking does the same. In all of them, the right-hand dict wins on
conflicting keys.
a = {"x": 1, "y": 2}
b = {"y": 9, "z": 3}
a | b # {'x': 1, 'y': 9, 'z': 3} — b's y wins
{**a, **b} # same result (works pre-3.9)
a.update(b) # mutates a in place
Pick | for a new merged dict, update/|= to modify one in place.
Views: keys, values, items
.keys(), .values(), and .items() return dynamic views, not lists — they reflect
later changes to the dict and avoid copying. Wrap in list(...) only if you need a
snapshot.
d = {"a": 1, "b": 2}
keys = d.keys()
d["c"] = 3
list(keys) # ['a', 'b', 'c'] — the view updated live
for k, v in d.items(): # idiomatic iteration
print(k, v)
Key views even support set operations (d1.keys() & d2.keys() for common keys), which is a
neat, lesser-known trick.
Dict comprehensions
Build a dict from an iterable in one expression — readable and fast.
squares = {n: n * n for n in range(5)} # {0:0, 1:1, 2:4, 3:9, 4:16}
inverted = {v: k for k, v in d.items()} # swap keys and values
Recap
A dict is a hash table giving average O(1) lookups, which is why keys must be
hashable/immutable (tuples, not lists). Since 3.7 dicts preserve insertion
order; use get/setdefault to handle missing keys without KeyError, the | operator
(or {**a, **b}) to merge with right-hand precedence, and the live views from
keys()/values()/items() for iteration. Reach for Counter/defaultdict when
counting or grouping. Understand the hash table underneath and every dict behaviour follows.