Dictionaries & Sets
Lists answer "what is at position 3?". Dictionaries answer "what is associated with this key?" — and they answer it in roughly constant time no matter how large they get. The calendar calls them maps. Once you have them, a whole class of slow list-scanning code disappears.
- Build, read and update a dictionary safely.
- Use the counting and grouping patterns from memory.
- Say which objects can be keys, and why.
- Choose between a list, a set and a dictionary deliberately.
Searching a list for a value means looking at every item. A dictionary jumps straight to the answer by computing where it must be. That difference is $O(n)$ versus $O(1)$, and on real data it is the difference between instant and unusable.
Every database index, every cache, every word-frequency count is a hash
table. Python's own namespaces are dictionaries — when you write
x = 5, an entry goes into one.
Hash tables date to 1953 at IBM. Since Python 3.7 dictionaries preserve insertion order, which was originally an implementation accident in 3.6 that proved so useful it was written into the language specification.
Dictionaries
A dictionary maps keys to values, written
{'a': 1, 'b': 2}. Keys are unique; assigning to an existing key
replaces its value. Lookup is d[key].
ages = {'ada': 36, 'alan': 41}
ages['ada'] # 36
ages['grace'] = 45 # adds a new pair
ages['ada'] = 37 # replaces the existing value
'alan' in ages # True - tests KEYS, not values
len(ages) # 3
del ages['alan'] # removes the pair
ages.keys() # the keys
ages.values() # the values
ages.items() # (key, value) pairsages['nobody'] is a KeyError. Three ways to
avoid it:
if k in d: before reading — clearest
when you need a branch anyway.d.get(k) returns None when
absent, or d.get(k, 0) to supply your own default.d.setdefault(k, []) inserts the default and
returns it, which is the grouping idiom below.for name in ages: # iterates KEYS by default
print(name, ages[name])
for name, age in ages.items(): # cleaner when you need both
print(name, age)Iterating a dictionary gives keys, not pairs. Reaching for
.items() when you want both is the idiom.
The two patterns worth memorising
def count_chars(s: str) -> dict:
"""Return a dictionary mapping each character of s to its count.
>>> count_chars('aab')
{'a': 2, 'b': 1}
"""
counts = {}
for ch in s:
counts[ch] = counts.get(ch, 0) + 1
return countsget(ch, 0) supplies zero the first time a character appears,
so the same line handles both the new and the repeat case. Without it you
need an if.
def group_by_length(words: list) -> dict:
"""Return a dictionary mapping each length to the words of that length.
>>> group_by_length(['at', 'to', 'cat'])
{2: ['at', 'to'], 3: ['cat']}
"""
groups = {}
for w in words:
groups.setdefault(len(w), []).append(w)
return groupssetdefault returns the list already stored, or inserts a new
empty one and returns that. Either way there is a list to append
to.
def invert(d: dict) -> dict:
"""Return a dictionary with keys and values swapped.
>>> invert({'a': 1, 'b': 2}) == {1: 'a', 2: 'b'}
True
"""
return {v: k for k, v in d.items()}Only safe when the values are unique and hashable. Duplicate values collapse, keeping whichever came last.
What can be a key
A key must be immutable: int, float,
str, bool, and tuples of those. Lists and
dictionaries cannot be keys.
The reason is direct. A dictionary places a key by computing a number from its contents. If the contents could change afterwards, the key would be filed in a location that no longer matches and would become unfindable.
grid = {}
grid[(0, 0)] = 'start' # fine - tuples are immutable
grid[[0, 0]] = 'start' # TypeError: unhashable type: 'list'Using coordinate tuples as keys is the standard way to represent a sparse grid without allocating every cell.
Sets
A set is an unordered collection of unique hashable values, written
{1, 2, 3}. Think of it as a dictionary with keys and no values.
The empty set is set(), since {} is an empty
dictionary.
a = {1, 2, 3}
b = {3, 4}
3 in a # True - fast, unlike a list scan
a | b # {1, 2, 3, 4} union
a & b # {3} intersection
a - b # {1, 2} difference
set([1, 1, 2]) # {1, 2} - deduplication in one step
sorted(set(xs)) # unique values, back in orderThe Unit 5 dedupe function scanned a growing list for every
item, which is quadratic. Tracking seen items in a set makes it linear, and
that is the whole reason to reach for one.
• {} is an empty dictionary, not an empty set. Use
set().
• Using a list as a key. Convert to a tuple first.
• Assuming in on a dictionary checks values. It checks
keys.
• Relying on set order. Sets have none, so
sorted() whenever output order matters.
• Changing a dictionary's size while iterating it. Iterate over
list(d.keys()) if you must delete during the loop.
Practice
Predict each answer before running it. Original exercises written for this guide in the style of the course's weekly sets.
For d = {'a': 1}, what are 'a' in d and
1 in d?
Answer
True and
False. Membership tests keys. Use
1 in d.values() for the other question.Count word frequencies in a sentence.
Solution
def word_counts(text: str) -> dict:
"""Return a dictionary mapping each word of text to its count.
>>> word_counts('a b a')
{'a': 2, 'b': 1}
"""
counts = {}
for w in text.split():
counts[w] = counts.get(w, 0) + 1
return countsWhat type is {}?
Answer
dict. The
empty set has to be written set(), because the brace notation
was taken by dictionaries first.Rewrite Unit 5's dedupe using a set, keeping order.
Solution
def dedupe(xs: list) -> list:
"""Return a new list with duplicates removed, order preserved.
>>> dedupe([1, 2, 1, 3])
[1, 2, 3]
"""
seen = set()
out = []
for x in xs:
if x not in seen:
seen.add(x)
out.append(x)
return outWhy does d[[1, 2]] = 'x' fail?
Answer
Find the key with the largest value in a non-empty dictionary.
Solution
def top_key(d: dict) -> str:
"""Return a key of d whose value is largest.
>>> top_key({'a': 1, 'b': 5})
'b'
"""
best = None
for k, v in d.items():
if best is None or v > d[best]:
best = k
return bestFind words appearing in both of two sentences.
Solution
set(a.split()) & set(b.split()). Intersection expresses
it directly; a nested loop would be quadratic and less
readable.Group words by their first letter.
Solution
def by_letter(words: list) -> dict:
"""Return a dictionary mapping first letters to the words starting
with them.
>>> by_letter(['ant', 'ape', 'bee'])
{'a': ['ant', 'ape'], 'b': ['bee']}
"""
groups = {}
for w in words:
groups.setdefault(w[0], []).append(w)
return groupsw[0]; guard it or
document that words are non-empty.Expanded topic checklist
Course coverage. This unit is part of the complete CSC108H1 topic map. It should cover the following items before you move on:
- Dictionaries. Create, read, update, delete; keys, values, items.
- Safe access.
in,getwith a default,setdefault. - Patterns. Counting, grouping, inverting.
- Sets. Uniqueness, membership, the algebra operations, and choosing between the three structures.
The calendar names "maps" among the elementary data types. Dictionary patterns are heavily assessed on assignments; check the syllabus on Quercus for the assigned exercises.
A dictionary trades memory for speed by computing where a key belongs rather than searching for it. Unit 9 makes that trade explicit as $O(1)$ versus $O(n)$.
x in some_list inside a loop, stop and ask
whether a set belongs there. It is the single most common speed fix in the
course.Deep practice extensions
Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.
What happens on d['absent']?
Guide
KeyError. Use d.get('absent', default) to avoid
it.Why must keys be immutable?
Guide
When is a set the right structure?
Guide
What does for x in d give?
Guide
d.items() for pairs, d.values() for
values.