Dictionaries: Look It Up 🗂️
If lists are a row of numbered boxes, a dictionary is a filing cabinet with labels. It is the container that real programs are actually made of.
Keys and values
pirate = {
"name": "Guybrush",
"role": "pirate",
"insults": 7,
"has_ship": False,
}
print(pirate["name"])
print(pirate["insults"])
print(len(pirate))
Guybrush
7
4
Curly braces, key: value pairs, commas between. You look things up by
key, not by position, which is why a dictionary stays readable when a
tuple of eight fields does not.
Adding, changing, removing
pirate = {"name": "Guybrush", "insults": 7}
pirate["ship"] = "Sea Monkey" # add
pirate["insults"] += 1 # change
print(pirate)
del pirate["ship"] # remove
print(pirate)
removed = pirate.pop("insults") # remove and give back
print(removed, pirate)
{'name': 'Guybrush', 'insults': 8, 'ship': 'Sea Monkey'}
{'name': 'Guybrush', 'insults': 8}
8 {'name': 'Guybrush'}
Assigning to a key that does not exist creates it. Assigning to one that does replaces it. There is no separate "add" and "update", which is one less thing to remember.
The missing key problem
pirate = {"name": "Guybrush"}
print(pirate["ship"])
KeyError: 'ship'
Three ways to handle it, in order of how often you want them:
pirate = {"name": "Guybrush"}
print(pirate.get("ship")) # None instead of an explosion
print(pirate.get("ship", "no ship yet")) # your own default
print("ship" in pirate) # just ask
if "name" in pirate:
print(f"Captain {pirate['name']}")
None
no ship yet
False
Captain Guybrush
Reach for .get(key, default) by default and square brackets only when a missing key genuinely means the program is broken. Letting it crash is sometimes right: silently defaulting a missing price to zero is worse than stopping.
Looping
scores = {"Guybrush": 95, "Elaine": 88, "Otis": 72}
for name in scores:
print(name)
print("---")
for name, score in scores.items():
print(f"{name:10} {score}")
print("---")
print(list(scores.keys()))
print(list(scores.values()))
print(sum(scores.values()))
Guybrush
Elaine
Otis
---
Guybrush 95
Elaine 88
Otis 72
---
['Guybrush', 'Elaine', 'Otis']
[95, 88, 72]
255
Looping over a dictionary gives you the keys. Almost always you want
.items(), which gives you both as a tuple that the for line
unpacks for you.
Since Python 3.7, dictionaries keep their insertion order as a language guarantee, not an accident. Before that they were officially unordered and code that relied on order was broken. If you read an old tutorial saying 'dictionaries have no order', it is describing a Python that no longer exists.
Sorting a dictionary
scores = {"Guybrush": 95, "Elaine": 88, "Otis": 72, "Meathook": 91}
for name, score in sorted(scores.items(), key=lambda pair: pair[1], reverse=True):
print(f"{score:3} {name}")
95 Guybrush
91 Meathook
88 Elaine
72 Otis
lambda pair: pair[1] is a tiny throwaway function meaning "the second part
of each pair", so we sort by score rather than by name. Lambdas get a proper treatment
in Lesson 37; for now, read it as "sort by this bit".
Counting things: the classic use
text = "the rubber chicken with a pulley in the middle"
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
for word, n in counts.items():
if n > 1:
print(f"{word}: {n}")
the: 2
counts.get(word, 0) + 1 is the counting idiom: "whatever it was, or zero if
new, plus one". Memorise it. The standard library also has a purpose-built tool:
from collections import Counter
text = "the rubber chicken with a pulley in the middle"
counts = Counter(text.split())
print(counts.most_common(3))
print(counts["the"])
print(counts["banana"]) # missing keys are 0, not an error
[('the', 2), ('rubber', 1), ('chicken', 1)]
2
0
What can be a key?
valid = {
"text": 1,
42: 2,
3.5: 3,
True: 4,
("x", "y"): 5, # tuples are fine: they cannot change
}
print(valid[("x", "y")])
# {["x", "y"]: 5} # TypeError: unhashable type: 'list'
5
Keys must be immutable. The reason is mechanical: a dictionary finds things instantly by computing a number from the key (a hash) and using it as an address. If the key could change afterwards, the address would be wrong and the value would be lost. Lists can change, so lists cannot be keys. Tuples cannot, so they can.
Looking up d["name"] in a dictionary of one item takes about the same time as in a dictionary of one million. Searching a list means checking items one by one, so a million-item list takes a million times longer than a one-item list. If you ever find yourself writing for x in big_list: if x.id == wanted inside another loop, a dictionary keyed by id will make your program dramatically faster. This is the single highest-value performance trick a beginner can learn.
Nested dictionaries: the shape of real data
game = {
"title": "The Secret of Monkey Island",
"year": 1990,
"characters": {
"Guybrush": {"role": "hero", "insults": 8},
"LeChuck": {"role": "villain", "insults": 3},
},
"islands": ["Melee", "Monkey"],
}
print(game["title"])
print(game["characters"]["Guybrush"]["insults"])
print(game["islands"][0])
for name, info in game["characters"].items():
print(f"{name:10} {info['role']:8} {info['insults']} insults")
The Secret of Monkey Island
8
Melee
Guybrush hero 8 insults
LeChuck villain 3 insults
Dictionaries containing dictionaries containing lists is exactly the shape of JSON, which is how essentially every web API on earth sends data. When you call an AI model in Level 6, this is what comes back. Get comfortable here and Level 5 becomes easy.
Phone book
Build a dictionary of three names to phone numbers. Look one up safely, handle a missing one, add a fourth, and print them all sorted by name.
Reveal solution
book = {
"Elaine": "555-0100",
"Guybrush": "555-0199",
"Otis": "555-0110",
}
print(book.get("Elaine"))
print(book.get("LeChuck", "not in the book"))
book["Meathook"] = "555-0123"
for name in sorted(book):
print(f"{name:10} {book[name]}")
555-0100
not in the book
Elaine 555-0100
Guybrush 555-0199
Meathook 555-0123
Otis 555-0110sorted(book) sorts the keys, because looping a dict gives keys. Short and idiomatic.
Letter frequency
Count how often each letter appears in a word, ignoring case, and print the counts in alphabetical order.
Reveal solution
word = "Mississippi"
counts = {}
for letter in word.lower():
counts[letter] = counts.get(letter, 0) + 1
for letter in sorted(counts):
print(f"{letter}: {counts[letter]}")
i: 4
m: 1
p: 2
s: 4Invert a dictionary
Turn {{'a': 1, 'b': 2, 'c': 3}} into {{1: 'a', 2: 'b', 3: 'c'}}. Then explain what happens if two keys share a value.
Reveal solution
original = {"a": 1, "b": 2, "c": 3}
flipped = {}
for key, value in original.items():
flipped[value] = key
print(flipped)
# and the catch
clash = {"a": 1, "b": 1}
flipped_clash = {}
for key, value in clash.items():
flipped_clash[value] = key
print(flipped_clash)
{1: 'a', 2: 'b', 3: 'c'}
{1: 'b'}The second one silently loses data: both keys map to 1, so the later one wins and 'a' vanishes. Inverting a dictionary is only safe when the values are unique, and noticing that before shipping is exactly the kind of thinking that separates working code from code that works today.