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.
- 1Encounter
- 2Understand
- 3Worked
- 4Predict
- 5Apply
- 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.
names = ["rafi", "ahmed", "bilal"]
marks = [72, 45, 90]
print(names[1], marks[1])ahmed 45It 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
KeyErrorwith.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:
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
print(marks)
print(marks["ahmed"])
print(len(marks)){'rafi': 72, 'ahmed': 45, 'bilal': 90}
45
3marks["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()
marks = {"rafi": 72}
print(marks["dia"])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():
marks = {"rafi": 72}
print(marks.get("dia"))
print(marks.get("dia", 0))
print(marks.get("rafi", 0))None
0
72The 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
marks = {"rafi": 72}
marks["dia"] = 88
marks["rafi"] = 75
print(marks)
del marks["rafi"]
print(marks){'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:
marks = {"rafi": 72, "rafi": 90}
print(marks){'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
marks = {"rafi": 72, "ahmed": 45}
print("rafi" in marks)
print(72 in marks)True
False72 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
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()))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():
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
print(sum(marks.values()))
print(max(marks.values()))
print(round(sum(marks.values()) / len(marks), 2))207
90
69.0What can be a key
To be a key, a thing has to be immutable. Text, numbers and tuples all work. A list does not:
d = {}
d[["a", "b"]] = 1TypeError: 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:
grid = {}
grid[(0, 0)] = "start"
grid[(1, 2)] = "tree"
print(grid)
print(grid[(1, 2)]){(0, 0): 'start', (1, 2): 'tree'}
treeThe last chapter called a tuple's immutability an advantage. This is the first practical proof of it.
The most useful pattern there is — counting
words = ["pen", "bag", "pen", "ink", "pen"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts){'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:
# 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)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.
Step 4 of 6 — Predict
Check your understanding
A loop runs directly over a dictionary. What is printed?
marks = {"rafi": 72, "ahmed": 45}
for name in marks:
print(name)- Arafi ahmed
- B72 45
- Crafi 72 ahmed 45
- D('rafi', 72) ('ahmed', 45)
A key that is not there is asked for. What happens, and what is the safe alternative?
marks = {"rafi": 72}
print(marks["dia"])- AA `KeyError` — the safe alternative is `marks.get("dia")`, which gives `None`
- BIt prints `None`
- CAn `IndexError`
- DIt prints a blank line
The counting pattern. What is printed?
words = ["pen", "bag", "pen"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)- A{'pen': 2, 'bag': 1}
- B{'pen': 1, 'bag': 1}
- C{'pen': 3, 'bag': 1}
- DA `KeyError`
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
Your turn
Write a file called stock.py holding a dictionary of at least six products — the product name as the key, the price as the value.
Then:
- Print each product and its price in aligned columns
- Print the total, the average, the highest and the lowest price
- Find the name of the most expensive product, not just the price
- Add a product, change one price, delete one product, then print the whole thing again
- Use
.get()to ask for a product that does not exist, with a message as the default
Then write a counting program: put several words in a list, some repeated, and count how many times each appears using the counts.get(word, 0) + 1 pattern.
Finally break it on purpose: ask for a missing key with square brackets, and try to use a list as a key. Two different errors will come back — explain to yourself which is which, and why.
Step 6 of 6
Stretch — the chapter quiz
Ten questions from easy to hard. The last ones are difficult on purpose.
Sign in to take the quiz