Building & Reshaping
Most dictionaries in real programs are not typed out — they are built from something else, or built from a dictionary that has the right facts in the wrong shape. Five patterns cover almost all of it, and every one of them is the same three lines: an empty dictionary, a loop, and one assignment.
1Program 1 — from two tuples that line up
Given a tuple of names and a tuple of marks in the same order, build a name-to-mark dictionary.
The two tuples are joined by position — names[2] belongs with marks[2] — so this is a job for the index loop:
# names[i] goes with marks[i]
names = ('Riya', 'Amit', 'Sara')
marks = (78, 85, 62)
report = {}
for i in range(len(names)):
report[names[i]] = marks[i]
print(report){'Riya': 78, 'Amit': 85, 'Sara': 62}report = {}The collector. Empty braces are an empty dictionary — () would be an empty tuple and [] an empty list.
for i in range(len(names)):One index, used on both tuples. The item form cannot do this: it hands over a name with no way to find the mark that goes with it.
report[names[i]] = marks[i]Assigning to a key that does not exist yet CREATES it. That is the whole build — there is no 'add' method to call.
marks is shorter, marks[i] raises IndexError: tuple index out of range partway through, and the dictionary is left half-built. Real code checks len(names) == len(marks) first.When the data already arrives as rows, it is simpler still — no index at all:
# from a tuple of records, which is the shape data usually has
rows = (('Riya', 78), ('Amit', 85), ('Sara', 62))
report = {}
for name, m in rows:
report[name] = m
print(report){'Riya': 78, 'Amit': 85, 'Sara': 62}2Program 2 — swapping keys and values
Turn a country-to-capital dictionary into a capital-to-country one.
# the value becomes the key, and the key becomes the value
capital = {'India': 'Delhi', 'France': 'Paris', 'Japan': 'Tokyo'}
country = {}
for k, v in capital.items():
country[v] = k
print(country){'Delhi': 'India', 'Paris': 'France', 'Tokyo': 'Japan'}country[v] = k — read it slowly. The value from the old dictionary is used as the key of the new one, which is why the line looks back to front.
# Riya and Sara both got an A — watch what happens
grade = {'Riya': 'A', 'Amit': 'B', 'Sara': 'A'}
by_grade = {}
for name, g in grade.items():
by_grade[g] = name
print(by_grade){'A': 'Sara', 'B': 'Amit'}Riya is gone. by_grade['A'] = 'Riya' ran, and then by_grade['A'] = 'Sara' overwrote it — because assigning to a key that already exists replaces its value. Nothing warned you.
The fix is to let each key hold a tuple of everyone who belongs to it, growing it as more arrive:
# each grade keeps everybody who got it
grade = {'Riya': 'A', 'Amit': 'B', 'Sara': 'A'}
by_grade = {}
for name, g in grade.items():
if g in by_grade:
by_grade[g] = by_grade[g] + (name,)
else:
by_grade[g] = (name,)
print(by_grade){'A': ('Riya', 'Sara'), 'B': ('Amit',)}The same shape as the frequency tally — first sighting makes the entry, later sightings add to it. The comma in (name,) is the same one-item-tuple comma from the tuples chapter, and leaving it out breaks this program in the same way.
3Program 3 — which keys have a given value?
Looking up by key is what a dictionary is for, and it takes one step. Looking up by value has no shortcut at all — it is a linear search, exactly like searching a tuple:
# who got an A?
grade = {'Riya': 'A', 'Amit': 'B', 'Sara': 'A', 'John': 'C'}
wanted = 'A'
for name, g in grade.items():
if g == wanted:
print(name)Riya Sara
grade['Riya'] goes straight there. “Everyone with an A” has to look at every pair. If a program keeps asking the value question, that is the sign it should have been built the other way round in the first place.4Program 4 — a new dictionary of the ones that qualify
# a smaller dictionary, built from a bigger one
salary = {'Riya': 42000, 'Amit': 55000, 'Sara': 38000, 'John': 61000}
seniors = {}
for name, pay in salary.items():
if pay > 50000:
seniors[name] = pay
print(seniors)
print(salary){'Amit': 55000, 'John': 61000}
{'Riya': 42000, 'Amit': 55000, 'Sara': 38000, 'John': 61000}The original is untouched — the loop only ever reads it. That is what makes filtering into a new dictionary the safe move, and it sidesteps the “changed size during iteration” error you would hit trying to delete the low earners as you walked.
5Program 5 — merging two dictionaries
# update() pours the second dictionary into the first
term1 = {'Riya': 78, 'Amit': 85}
term2 = {'Sara': 62, 'Amit': 90}
term1.update(term2)
print(term1){'Riya': 78, 'Amit': 90, 'Sara': 62}Sara is new and gets added. Amit is in both, and the second dictionary wins — 85 became 90. That is the same overwrite rule as before, and here it is usually what you want.
update() changes term1 itself. The original term-1 marks are gone. When both are still needed, copy first:# keep both: merge into a copy
term1 = {'Riya': 78, 'Amit': 85}
term2 = {'Sara': 62, 'Amit': 90}
both = term1.copy()
both.update(term2)
print('term1:', term1)
print('both: ', both)term1: {'Riya': 78, 'Amit': 85}
both: {'Riya': 78, 'Amit': 90, 'Sara': 62}6Program 6 — grouping a table by one column
The most useful of the lot, and the one that shows up in every data question ever asked: a table of rows, and a total per category.
Given a tuple of (region, sales) rows, total the sales for each region.
# one running total per region, made as each region first appears
sales = (('north', 1200), ('south', 900), ('north', 800),
('east', 400), ('south', 600))
totals = {}
for region, amount in sales:
totals[region] = totals.get(region, 0) + amount
for region in sorted(totals):
print(region, '->', totals[region])east -> 400 north -> 2000 south -> 1500
+ amount instead of + 1. That is the whole difference between counting and totalling, and it is the same difference as count = count + 1 versus total = total + m on a plain loop. Recognising it means you already know how to write this one.# the counting version: how many students in each section?
students = (('Riya', 'A'), ('Amit', 'B'), ('Sara', 'A'),
('John', 'C'), ('Zoya', 'A'))
tally = {}
for name, sec in students:
tally[sec] = tally.get(sec, 0) + 1
for sec in sorted(tally):
print('Section', sec, '-', tally[sec], 'student(s)')Section A - 3 student(s) Section B - 1 student(s) Section C - 1 student(s)
7Recap
d[k] = v is both 'add' and 'change'. There is no separate add method, and that is why an overwrite is so easy to do by accident.
Two keys sharing a value means one of them vanishes on the swap. Keep a tuple per key when that can happen.
d[k] is one step; 'which keys hold this value' has to look at every pair. Build it the other way round if you keep asking.
Deleting as you walk raises RuntimeError. Building a second one leaves the original intact and cannot go wrong.
The same line as the frequency tally, with the amount instead of 1. Counting and totalling per category are one pattern.
- 1
Build a dictionary of squares —
{1: 1, 2: 4, 3: 9, …}— for 1 to 10.Hint ·
for n in range(1, 11):andsquares[n] = n * n. Nothing to loop over but the numbers. - 2
From a tuple of
(item, price, qty)rows, build an item-to-line-total dictionary.Hint · Three names in the loop header, and
totals[item] = price * qty. - 3
Build a dictionary of the items priced under 25.
Hint · Program 4, with the test turned round.
- 4
Invert a phone book, and say what happens if two people share a number.
Hint · One of them disappears. Try the tuple-per-key version from program 2 to keep both.
- 5
From a tuple of
(city, temperature)readings taken on many days, print the highest temperature per city.Hint · A champion per key: if the city is new, store the reading; otherwise store it only when it beats what is there.
grade = {'Riya': 'A', 'Amit': 'B', 'Sara': 'A'}. What does the plain inversion produce?
Why build a filtered dictionary rather than delete from the original while looping?
What is the difference between the counting tally and the grouping total?