Dictionaries for Counting and Grouping
Learning Objectives
- You can count occurrences of each distinct value using a dictionary.
- You can accumulate a running total per category in a single pass.
- You can recognise the “build a dictionary as you go” pattern and apply it to grouping problems.
- You can compare using dictionaries to the two-pass list approach, and explain why this is shorter and faster than the two-pass list approach.
In the Summaries by Category chapter of the last part, you wrote a totals_by_subject function that first collected the distinct subjects in a list, and then looped over that list to total the minutes for each subject.
Here is the shape of the function:
def totals_by_subject(sessions): subjects_seen = [] for subject, minutes, day in sessions: if subject not in subjects_seen: subjects_seen.append(subject)
result = [] for subject in subjects_seen: total = 0 for row_subject, minutes, day in sessions: if row_subject == subject: total += minutes result.append([subject, total]) return resultThe function does two passes. The first walks every session to collect the distinct subjects. The second walks every session again, once per subject, to total the minutes. For a hundred sessions across ten subjects, the inner loop runs a thousand times.
The reason it had to be done this way is that a list has no way to say “the running total for math” as there is no slot named math.
A dictionary has exactly the feature this problem needs, because it can say “the running total for math is totals["math"]”. The dictionary can hold the running total for each subject under a key of that subject’s name. When you see a session, you look up its subject in the dictionary and add to that slot.
Counting With a Dictionary
Start with the simplest version of the problem: counting. How many sessions are there for each subject?
The pattern is to build a dictionary as you walk the data once. The keys are subjects; the values are running counts.
def count_by_subject(sessions): counts = {} for subject, minutes, day in sessions: counts[subject] = counts.get(subject, 0) + 1 return countsLet’s walk through it. counts starts empty. For each session, you look up its subject in counts to get the current count. If the subject is not a key — it has not been seen before — .get() returns 0, which is the right starting point for counting. You add one to that count and store it back under the same key.
Starting a running count
0 / 5 points
In counts[kind] = counts.get(kind, 0) + 1, what is the role of 0?
The dictionary holds the running count for each subject as you go. When you are done, it has the final count for each subject.
For the usual fixture:
sessions = [ ["math", 45, "2024-03-10"], ["physics", 90, "2024-03-10"], ["history", 30, "2024-03-11"], ["math", 60, "2024-03-11"], ["math", 25, "2024-03-12"],]
print(count_by_subject(sessions))# {'math': 3, 'physics': 1, 'history': 1}One pass. No subjects_seen list, no inner loop, no re-scanning. The dictionary is the mapping from subject to count, built up as you go.
The above code uses the “check-then-set” pattern: check if the key is there, and if it is, add to it; otherwise start it. This is a common pattern when building a dictionary as you go. The .get() method is a convenient way to shorten this pattern, as you will see in the next section.
You could also write the getting and setting with two lines:
count = counts.get(subject, 0)counts[subject] = count + 1The one-liner is more common, but the two-liner can be clearer when the contribution is more complex than + 1.
Count borrowed genres
0 / 5 points
Arrange the lines into a function that counts how many borrowed books there are for each genre.
Each book is a dictionary with a "genre" key. The function must return a dictionary such as {"history": 2, "fiction": 1}.
If/Else Instead of Check-Then-Set
An alternative to the check-then-set pattern is to use an if/else:
def count_by_subject(sessions): counts = {} for subject, minutes, day in sessions: if subject in counts: counts[subject] += 1 else: counts[subject] = 1 return countsOr, just an if that sets the count to 0 if the key is missing, and then adds one:
def count_by_subject(sessions): counts = {} for subject, minutes, day in sessions: if subject not in counts: counts[subject] = 0 counts[subject] += 1 return countsTotalling With a Dictionary
Counting adds 1 each time. Totalling adds the minutes each time. That is the only change:
def totals_by_subject(sessions): totals = {} for subject, minutes, day in sessions: totals[subject] = totals.get(subject, 0) + minutes return totalsCompare this to the two-pass version at the top of the chapter. Same result, but the function went from twelve lines with a nested loop to four lines with a single pass:
print(totals_by_subject(sessions))# {'math': 130, 'physics': 90, 'history': 30}Math’s total is 45 + 60 + 25 = 130, accumulated across the three times "math" came up as the loop walked the sessions once. The dictionary held the running total under the key "math" the whole way through.
Building Several Summaries at Once
In Summaries by Category, the function summaries_by_subject returned count, total, and average together. The function required first collecting the subjects, then looping once per subject to compute the count, the total, and the average.
The dictionary version can do all of that in one pass, because it can hold multiple running values for each subject at the same time. The count and total are accumulated together in the first loop, and the average is computed at the end once the counts and totals are final:
def summaries_by_subject(sessions): counts = {} totals = {} for subject, minutes, day in sessions: counts[subject] = counts.get(subject, 0) + 1 totals[subject] = totals.get(subject, 0) + minutes
result = {} for subject in counts: result[subject] = { "count": counts[subject], "total": totals[subject], "average": totals[subject] / counts[subject], } return resultThe first loop accumulates counts and totals in one pass. The second loop — which runs once per subject, not once per session — assembles the final summary. Notice that the inner value is itself a dictionary: result["math"] is {"count": 3, "total": 130, "average": 43.33...}. A dictionary whose values are themselves dictionaries is a normal and useful shape, and the next chapter looks at it more closely.
summaries = summaries_by_subject(sessions)print(summaries["math"])# {'count': 3, 'total': 130, 'average': 43.333333333333336}The average is computed only at the end, once each subject’s count and total are final — never inside the accumulation loop, where the count is still partial.
The Pattern in This Chapter
Every example in this chapter is the same shape:
result = {}for item in data: key = something_about(item) result[key] = result.get(key, starting_value) + contribution(item)Start with an empty dictionary. Walk the data once. For each item, work out which key it belongs to, and fold its contribution into that key’s running value. For counting, the contribution is 1 and the starting value is 0. For totalling, the contribution is the minutes.
This is one of the most common patterns in all of programming — grouping a pile of records by some key and summarising each group. You will reach for it constantly. It is worth recognising the moment a problem says “per subject”, “per day”, “per user”, “for each category”: that is a dictionary accumulation, one pass, keyed by the category.
Stats from file
0 / 20 points
Write a program that reads product ratings from data.csv and prints the average rating for each product.
The file has this header:
category,product,ratingRequired helpers:
| Helper | Contract |
|---|---|
load_ratings(path) | read the CSV file at path and return rows shaped as [category, product, rating], where rating is a float |
average_ratings_by_product(rows) | return a dictionary where each key is a product and each value is that product’s average rating |
Requirements:
- use the
csvmodule andcsv.reader, - open files with
encoding="utf-8", - skip the header row,
- convert ratings to floats,
- print one line per product,
- print products in alphabetical order,
- format each average with one decimal place (
f"{average:.1f}"), - do not use
input(...), - and at the bottom of the program, read
data.csvand print the product averages.
For this data.csv:
category,product,ratingschool,Notebook,5home,Desk Lamp,4school,Notebook,4home,Desk Lamp,5the program should print:
Desk Lamp: 4.5Notebook: 4.5Dictionaries Versus Two-Pass for Grouping
The two-pass list version that we used in the previous part was not wrong. It produced the right answer. It, however, had two costs that the dictionary version sheds.
-
It was slower in a way that grows: re-scanning all the sessions once per subject means the work multiplies as both the data and the number of subjects grow. The dictionary version touches each session once, full stop.
-
It was harder to read. The two-pass version made you hold “we collect the subjects first, then loop again for each one” in your head. The dictionary version reads straight down: for each session, add its minutes to its subject’s total.
The next chapter looks at the richer shapes you can build once values can themselves be lists or dictionaries.
Check Your Understanding
- In the line
counts[subject] = counts.get(subject, 0) + 1, what is the role of the0? - The two-pass list version of
totals_by_subjectwas slower “in a way that grows”. Grows with what? - How would you change the totalling loop so that it counts sessions per subject instead of summing minutes?
- Why is the average in
summaries_by_subjectcomputed in the second loop, rather than inside the first accumulation loop? - A problem asks you to report the number of orders per customer. What data shape and what pattern does that call for?
AI Study Aid
Create a chapter diagram
We're looking into to what extent AI-generated diagrams could help with studying.
Use this study aid to generate an AI-generated visual summary of the material you just studied.
Each diagram style emphasizes the content in a different way, so you can choose the focus
that feels most useful.
Using the diagram generator is voluntary and does not affect your course progress. We encourage
you to try it out and see if it helps, but it's totally up to you! Your answers help us understand
how to make better study aids in the future.