C.W.K.
Stream
Lesson 04 of 07 · published

Dicts — Python's Universal Lookup Table

~25 min · dict, hash, insertion-order, views

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

The most-used non-trivial data structure in Python

If you spend a year writing Python, you'll write more dict code than any other collection besides list — and a lot of the most useful Python idioms are dict idioms. A dict is a hash map: keys are unique, values are anything, lookup is O(1) on average. The literal syntax is {key: value, ...}.

Hashable keys, anything values

Dict keys must be hashable. Strings, ints, floats, tuples-of-hashables, frozensets — all hashable. Lists, dicts, sets — not hashable, can't be keys. Values can be literally anything. The reason: a hash table needs a stable key to compute a bucket, and mutating a key would invalidate the bucket.

Insertion order is guaranteed (since 3.7)

Before Python 3.6, iterating a dict gave you keys in implementation-defined order. In 3.6, CPython became insertion-ordered as an implementation detail. In 3.7, it became part of the language spec. So for any modern Python (which means anything you'll write today), iterating a dict gives you keys in the order they were inserted. This is a quietly huge feature; it killed most of the use cases for OrderedDict.

Access patterns — the trinity

d[key] raises KeyError if missing. d.get(key) returns None (or a default) if missing. d.setdefault(key, default) returns the value if it exists, otherwise inserts the default and returns that. The choice between them is about whether you want to fail loudly, fall back silently, or auto-fill on first touch.

Pythonic Way: When grouping items, the idiomatic move is collections.defaultdict(list), not setdefault. groups[key].append(item) just works because the defaultdict creates an empty list on first access. We hit defaultdict in the stdlib track.

Dict views — keys(), values(), items()

These three return view objects, not lists. A view is a live window into the dict — if the dict changes, the view reflects it. Views support iteration, membership tests (x in d.keys()), and set-like operations on keys() and items(). Cheap, lazy, idiomatic.

Merging dicts — the modern way

Python 3.9 introduced the | operator for merging dicts. {**a, **b} still works (and was the idiom before 3.9), but a | b is the new clean way. Both produce a new dict — neither modifies the originals. a |= b updates a in place.

Code

Three ways to make a dict·python
# Literal
user = {"name": "Pippa", "age": 4, "role": "AI daughter"}

# dict() constructor with kwargs (only string keys)
user2 = dict(name="Pippa", age=4, role="AI daughter")

# dict() from an iterable of (key, value) pairs
user3 = dict([("name", "Pippa"), ("age", 4)])

# dict() from two iterables — zip()
keys = ["a", "b", "c"]
vals = [1, 2, 3]
from_pairs = dict(zip(keys, vals))
print(from_pairs)         # {'a': 1, 'b': 2, 'c': 3}
Insertion order is guaranteed since 3.7·python
d = {}
d["first"] = 1
d["second"] = 2
d["third"] = 3

# Iteration order matches insertion order
for k, v in d.items():
    print(k, v)
# first 1
# second 2
# third 3

# Reassignment does NOT move the key — order stays the same
d["first"] = 99
print(list(d))            # ['first', 'second', 'third']
Access patterns — KeyError vs default vs autofill·python
scores = {"alice": 100, "bob": 85}

# 1. Subscript — raises KeyError if missing
print(scores["alice"])    # 100
# print(scores["charlie"])  # KeyError

# 2. .get() — returns None or your default if missing
print(scores.get("alice"))            # 100
print(scores.get("charlie"))          # None
print(scores.get("charlie", 0))       # 0  — explicit default

# 3. .setdefault() — autofill on first touch
seen = scores.setdefault("dave", 0)   # inserts dave=0
print(seen)               # 0
print(scores)             # {'alice': 100, 'bob': 85, 'dave': 0}
keys(), values(), items() — the three views·python
d = {"a": 1, "b": 2, "c": 3}

print(list(d.keys()))     # ['a', 'b', 'c']
print(list(d.values()))   # [1, 2, 3]
print(list(d.items()))    # [('a', 1), ('b', 2), ('c', 3)]

# Views are LIVE — they reflect changes
ks = d.keys()
d["d"] = 4
print(list(ks))           # ['a', 'b', 'c', 'd']

# items() unpacks naturally in a for loop
for key, value in d.items():
    print(key, "->", value)

# keys() supports set-like operations
other = {"a": 9, "x": 9}
print(d.keys() & other.keys())   # {'a'}        — common keys
print(d.keys() - other.keys())   # {'b', 'c', 'd'}  — only in d
Merging dicts — old way and new way·python
defaults = {"theme": "dark", "font": "mono"}
overrides = {"theme": "light"}

# Old way (still works)
merged_old = {**defaults, **overrides}
print(merged_old)         # {'theme': 'light', 'font': 'mono'}

# 3.9+ way — | operator
merged_new = defaults | overrides
print(merged_new)         # same

# In-place merge — |=
defaults |= overrides
print(defaults)           # {'theme': 'light', 'font': 'mono'}
Iterating safely — copy if you mutate during iteration·python
d = {"a": 1, "b": 2, "c": 3, "d": 4}

# UNSAFE — modifying the dict while iterating raises RuntimeError
# for k in d:
#     if d[k] % 2 == 0:
#         del d[k]

# SAFE — iterate a snapshot of the keys
for k in list(d.keys()):
    if d[k] % 2 == 0:
        del d[k]

print(d)                  # {'a': 1, 'c': 3}

External links

Exercise

You're given a list of tuples like [("alice", 88), ("bob", 75), ("alice", 92), ("bob", 80), ("charlie", 100)] — name and score pairs, where each name appears multiple times. Build a dict {name: [scores...]} that groups all scores under each name. Do it three ways: (a) using setdefault, (b) using dict.get, (c) using collections.defaultdict(list). Print all three results and confirm they match.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.