Chapter 15

Dictionaries — pairs of key and value

Storing and fetching values by name, avoiding KeyError with .get(), adding and deleting keys, walking with .items(), and why a list cannot be a key.

32 minPython 3.12
  1. 1Encounter
  2. 2Understand
  3. 3Worked
  4. 4Predict
  5. 5Apply
  6. 6Stretch

The problem we are solving

Three people's marks need storing — with whose mark is whose.

Lists offer only one way: two lists, side by side.

python
names = ["rafi", "ahmed", "bilal"]
marks = [72, 45, 90]

print(names[1], marks[1])
text
ahmed 45

It works. And the whole arrangement rests on your discipline, with no help from Python at all.

Add someone to names and forget marks? The two lists are now different lengths, and from there names[2] and marks[2] describe two different people. Python will not say a word. Sort names? The relationship breaks completely, and silently.

Here is the root of it: the relationship between the two things is written down nowhere. The relationship is only "the same index" — a coincidence that can break at any moment.

A dictionary writes the relationship into the code. Names instead of indexes, and the pair held together in one place.

By the end of this chapter you can

  • Build a dictionary and fetch a value by its key
  • Avoid a KeyError with .get(), and know when each is right
  • Add, change and delete keys
  • Walk a dictionary with .keys(), .values() and .items()
  • Explain why a list cannot be a key but a tuple can

Prerequisites: Tuples and unpacking.


A dictionary — pairs of key and value

Curly brackets, with key: value pairs inside:

python
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}

print(marks)
print(marks["ahmed"])
print(len(marks))
text
{'rafi': 72, 'ahmed': 45, 'bilal': 90}
45
3

marks["ahmed"] — a name, not an index. And the name is right there in the code, so six months later it still says what is being asked for.

len() counts the pairs: three.

A word about order. Since Python 3.7 a dictionary remembers the order things were put in, and prints them that way. But a dictionary is for looking things up by name, not by position — write marks[0] and Python will look for a key that is the number zero, not find one, and stop.

A key that is not there — KeyError and .get()

python
marks = {"rafi": 72}
print(marks["dia"])
text
KeyError: 'dia'

A KeyError means "there is no key by that name". The same mistake on a list gave an IndexError — a different name for the same kind of error.

To get a safe answer instead of a stop, use .get():

python
marks = {"rafi": 72}

print(marks.get("dia"))
print(marks.get("dia", 0))
print(marks.get("rafi", 0))
text
None
0
72

The second argument is "give me this if it is missing". Without one you get None.

Which, when? If the key is supposed to be there, write marks["rafi"] — its absence is a bug, and the KeyError hands it to you immediately. If the key being absent is normal, use .get(). Reaching for .get() everywhere lets bad data slide through quietly, and bad data is what you were trying to find.

Adding, changing, deleting

python
marks = {"rafi": 72}

marks["dia"] = 88
marks["rafi"] = 75

print(marks)

del marks["rafi"]
print(marks)
text
{'rafi': 75, 'dia': 88}
{'dia': 88}

The same line does two jobs: if the key is missing it is added, and if it exists it is replaced. There is no separate "add" method.

And out of that comes an important rule — a key can only appear once:

python
marks = {"rafi": 72, "rafi": 90}
print(marks)
text
{'rafi': 90}

No error. The second quietly erased the first. This happens routinely when loading data from a file, and is a common way records go missing.

in searches the keys, not the values

python
marks = {"rafi": 72, "ahmed": 45}

print("rafi" in marks)
print(72 in marks)
text
True
False

72 is certainly there as a value, but in looks only among the keys. To search the values you have to say so: 72 in marks.values().

Walking it, three ways

python
marks = {"rafi": 72, "ahmed": 45}

for name in marks:
    print(name)

print("---")
for name, mark in marks.items():
    print(name, mark)

print("---")
print(list(marks.keys()))
print(list(marks.values()))
text
rafi
ahmed
---
rafi 72
ahmed 45
---
['rafi', 'ahmed']
[72, 45]

The first is worth noticing: for name in marks gives you the keys, not the values. It surprises everybody the first time.

When you want both, .items() hands back a tuple each time round — and name, mark unpacks it. This is where the last chapter's unpacking earns its keep.

To calculate over the values, .values():

python
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}

print(sum(marks.values()))
print(max(marks.values()))
print(round(sum(marks.values()) / len(marks), 2))
text
207
90
69.0

What can be a key

To be a key, a thing has to be immutable. Text, numbers and tuples all work. A list does not:

python
d = {}
d[["a", "b"]] = 1
text
TypeError: unhashable type: 'list'

That word "unhashable" is worth unpacking. A dictionary finds things so fast because it computes a number from each key and uses that number to go straight to the right place. If the key could change afterwards, that number would be wrong and the entry would be lost. So Python refuses to let anything changeable be a key at all.

A tuple cannot change, which makes it an excellent key:

python
grid = {}
grid[(0, 0)] = "start"
grid[(1, 2)] = "tree"

print(grid)
print(grid[(1, 2)])
text
{(0, 0): 'start', (1, 2): 'tree'}
tree

The last chapter called a tuple's immutability an advantage. This is the first practical proof of it.

The most useful pattern there is — counting

python
words = ["pen", "bag", "pen", "ink", "pen"]
counts = {}

for word in words:
    counts[word] = counts.get(word, 0) + 1

print(counts)
text
{'pen': 3, 'bag': 1, 'ink': 1}

Read the line from the inside: counts.get(word, 0) says "give me this word's count so far, and zero if it has none yet", then one is added and the result put back.

.get()'s default is doing all the work here — without it, every new word would raise a KeyError and you would need if word in counts: and two branches. You will write this one-line pattern many times.


A complete example

marks.py:

python
# A dictionary keeps a name attached to its value
marks = {"rafi": 72, "ahmed": 45, "bilal": 90, "dia": 88}

print("Everyone:")
for name, mark in marks.items():
    status = "pass" if mark >= 40 else "fail"
    print(f"  {name:<8} {mark:>3}  {status}")

print()
print("Count    :", len(marks))
print("Total    :", sum(marks.values()))
print("Average  :", round(sum(marks.values()) / len(marks), 2))
print("Highest  :", max(marks.values()))

best = max(marks, key=marks.get)
print("Best     :", best, marks[best])

print("Has dia  :", "dia" in marks)
print("Missing  :", marks.get("nadia", "not recorded"))

marks["dia"] = 91
marks["nadia"] = 60
del marks["ahmed"]

print()
print("After edits:", marks)
text
Everyone:
  rafi      72  pass
  ahmed     45  pass
  bilal     90  pass
  dia       88  pass

Count    : 4
Total    : 295
Average  : 73.75
Highest  : 90
Best     : bilal 90
Has dia  : True
Missing  : not recorded

After edits: {'rafi': 72, 'bilal': 90, 'dia': 91, 'nadia': 60}

Three things worth noticing.

status = "pass" if mark >= 40 else "fail" is a way of writing a condition on one line, read from the middle outwards: ""pass" if mark >= 40, otherwise "fail"". Chapter nine's four-line if/else is here choosing a single value, so one line is enough. Write a full if for a real decision — this form is only for picking one of two values.

max(marks, key=marks.get) is chapter thirteen's key= doing a new job. max(marks) would give the largest key, meaning the last name alphabetically. key=marks.get says to compare each key by its value instead, which gives the name with the highest mark. Notice that getting the highest mark is easy (max(marks.values())); finding out whose it is needs this trick.

The order on the last line. dia already existed, so its value changed but its position did not. nadia is new, so it was added at the end. And ahmed is gone. A dictionary remembers insertion order, and replacing a value does not disturb it.


When it breaks

KeyError: 'dia' There is no key by that name. Check spelling and case — "Rafi" and "rafi" are two different keys. Keys read from a file often carry a trailing space; print(list(d.keys())) puts quotes around each one so it shows. If the key being absent is normal, use .get().

TypeError: unhashable type: 'list' An attempt to use a list as a key. Use a tuple — tuple(my_list) makes one.

for x in my_dict is not giving me the values Nor should it — that gives the keys. For values use .values(), and for both .items().

Two keys were the same and a record vanished A key exists only once, and the later one silently replaced the earlier. To hold several records under one name, make the value a list — chapter seventeen's subject.

TypeError: 'dict' object is not subscriptable, or KeyError: 0 Something wrote my_dict[0]. A dictionary has no positions; 0 is looked up as a key.

.sort() does not work on a dictionary Correct — dictionaries have no .sort(). sorted(d.items()) gives a sorted list, which is usually what you wanted.