Level 2 · The Toolbox

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
🎯 .get() is the one you want

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.

ENCYCLOPEDIA[Medium: Success]

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.

⚡ Why dictionaries are fast

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.

Exercise 1

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-0110

sorted(book) sorts the keys, because looping a dict gives keys. Short and idiomatic.

Exercise 2

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: 4
Exercise 3

Invert 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.

+100 XP