Project: Build a Gradebook Report
Build a small gradebook report from CSV data with validated scores, averages, and deterministic ranking.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Project: Build a Gradebook Report
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
We have a tiny CSV gradebook with three rows. Ada appears twice because she completed two assessments. Lin appears once. Our goal is one report row per student, containing the number of scores and the average, with the highest average first. Before writing the function, predict Ada's average and decide how tied averages should appear. This project joins several beginner ideas: parsing text, validating it, grouping repeated keys, calculating averages, and sorting structured results. The completed function will return data rather than print a formatted table, so another program can reuse it. A gradebook is a useful small project because it combines file-format parsing,
- 00:44
validation, grouping, arithmetic, and sorting. The input has student and score columns, and a student may appear on several rows. The report should show one row per student with a count and average. csv.DictReader handles the CSV syntax, but it returns each score as text. Adding text values fails or concatenates them depending on the operation. Convert each score to an integer only after checking the column exists, then require a value from zero through one hundred. Decide and document what an empty input means before building the aggregation. The broken run tries to sum score cells straight from DictReader.
- 01:27
Those cells are strings, not integers, so Python raises TypeError. In other programs, string concatenation might create a plausible-looking but wrong result. The parser's job is to understand CSV quoting and columns; it cannot know whether your score is a number between zero and one hundred. That requirement belongs to the application. Inspect one row, read its types, and then put conversion and range checks at the boundary before any score contributes to a total. Start with a deliberately broken sum over score strings. The TypeError points to the boundary between parsed text and numerical data. A robust input loop checks the exact two- column header, trims student names, parses
- 02:11
scores, and raises ValueError with a clear message for a blank name, a missing score, a non-integer score, or an out-of-range value. DictReader may place extra cells under a None key or missing cells under a field whose value is None. Reject both. This small project should fail predictably on malformed records rather than produce an attractive but incorrect average. A good import loop checks the exact header first. It rejects a row with an extra or missing cell, trims the student name, and converts score with int. We then require zero through one hundred inclusive. A blank name or out-of-range number raises ValueError with a useful message.
- 02:56
This makes it impossible for a malformed row to create a misleading average. As each valid row arrives, the totals dictionary stores a pair: current sum and count. Repeated names update the same entry, which is why Ada produces one report row rather than two. For each valid row, keep a running total and count under the student's name. A dictionary of dictionaries is enough; no database is needed. After reading all rows, create one output dictionary per student with name, count, and average rounded to one decimal place. Sort reports by descending average and then ascending student name so ties are stable.
- 03:37
The sort key (-average, student) states the order plainly. Returning structured data makes the function easy to test and lets a separate interface decide whether to print a table or save JSON. After the loop, divide each student's total by count and round to one decimal place. Then sort by a tuple: negative average and student name. Negating the average puts high values first, while the name resolves ties alphabetically. Returning a list of dictionaries makes the ranking visible and testable. The sample prints Ada at 85.0 and Lin at 80.0. A full production gradebook might need weighted assessments or decimal rounding
- 04:18
rules; this exercise promises only an unweighted arithmetic average of validated integer scores. State that promise so users do not infer a grading policy we never implemented. There are two separate deterministic choices: a one-decimal average and a name tie-breaker. Rounding happens after dividing the total by the count, not on each score. For this learning project, integers ensure no parsing ambiguity and the Python round function meets the stated contract. Real schools may define different rounding, weighting, missing-assessment, and duplicate-record rules. Those should be written down before reusing the tool for decisions that affect
- 04:59
people. Quoted student names can contain commas because DictReader handles CSV syntax. A duplicate row for the same student is counted as another score because the input represents assessments; if duplicates were accidental, the schema would need an assessment identifier. The project demonstrates how validation and explicit rules prevent a polished report from concealing incorrect source data. The verification includes two tied students, then an invalid score of 101. Ties should be alphabetical, and 101 should fail before it affects totals.
- 05:39
Also test a header with no records; it returns an empty report. Run the browser task, which checks malformed headers and rows in addition to the sample. Take the quiz about CSV text values, grouping, and sorting. If you extend this into a local file project, open the CSV with newline='', pass it to DictReader, and keep the core report function independent of file location and presentation format. The checked task implements gradebook_report(csv_text). It returns an empty list for a valid header with no rows, and raises ValueError for malformed data. The sample with two Ada scores and one Lin score should return Ada at 85.0 ahead of
- 06:21
Lin at 80.0. Test repeated students, a quoted student name, tied averages, zero and one hundred, empty input, and invalid scores. The exercise checks both a happy path and several failure paths. The lesson's source link points to the relevant csv documentation so you can expand this project into a local command- line tool later.
Understand the concept
A gradebook is a useful small project because it combines file-format parsing, validation, grouping, arithmetic, and sorting. The input has student and score columns, and a student may appear on several rows. The report should show one row per student with a count and average. csv.DictReader handles the CSV syntax, but it returns each score as text. Adding text values fails or concatenates them depending on the operation. Convert each score to an integer only after checking the column exists, then require a value from zero through one hundred. Decide and document what an empty input means before building the aggregation.
Start with a deliberately broken sum over score strings. The TypeError points to the boundary between parsed text and numerical data. A robust input loop checks the exact two-column header, trims student names, parses scores, and raises ValueError with a clear message for a blank name, a missing score, a non-integer score, or an out-of-range value. DictReader may place extra cells under a None key or missing cells under a field whose value is None. Reject both. This small project should fail predictably on malformed records rather than produce an attractive but incorrect average.
For each valid row, keep a running total and count under the student's name. A dictionary of dictionaries is enough; no database is needed. After reading all rows, create one output dictionary per student with name, count, and average rounded to one decimal place. Sort reports by descending average and then ascending student name so ties are stable. The sort key (-average, student) states the order plainly. Returning structured data makes the function easy to test and lets a separate interface decide whether to print a table or save JSON.
The checked task implements gradebook_report(csv_text). It returns an empty list for a valid header with no rows, and raises ValueError for malformed data. The sample with two Ada scores and one Lin score should return Ada at 85.0 ahead of Lin at 80.0. Test repeated students, a quoted student name, tied averages, zero and one hundred, empty input, and invalid scores. The exercise checks both a happy path and several failure paths. The lesson's source link points to the relevant csv documentation so you can expand this project into a local command-line tool later.
- DictReader parses named CSV columns
- Validate before arithmetic
- Aggregate total and count per student
- Sort averages descending with a name tie-breaker
See it step by step
Read the code, predict the output, then compare it with the result.
01. Read named score cells
import csv, io
text='student,score\nAda,90\n'
print(list(csv.DictReader(io.StringIO(text))))
[{'student': 'Ada', 'score': '90'}]DictReader maps header names to text values, so 90 is still a string.
02. Aggregate repeated students
rows=[('Ada',90),('Lin',80),('Ada',80)]
totals={}
for name,score in rows:
total,count=totals.get(name,(0,0))
totals[name]=(total+score,count+1)
print(totals)
{'Ada': (170, 2), 'Lin': (80, 1)}A running sum and count preserve enough information to calculate each average.
03. Sort averages with stable ties
report=[{'student':'Zoe','average':90.0},{'student':'Ada','average':90.0},{'student':'Lin','average':80.0}]
print([row['student'] for row in sorted(report,key=lambda row:(-row['average'],row['student']))])
['Ada', 'Zoe', 'Lin']The negative average ranks high scores first; the name resolves ties.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Parse score text
A gradebook is a useful small project because it combines file-format parsing, validation, grouping, arithmetic, and sorting. The input has student and score columns, and a student may appear on several rows. The report should show one row per student with a count and average. csv.DictReader handles the CSV syntax, but it returns each score as text. Adding text values fails or concatenates them depending on the operation. Convert each score to an integer only after checking the column exists, then require a value from zero through one hundred. Decide and document what an empty input means before building the aggregation.
A CSV cell is text even if it looks numeric. Try a non-integer score and decide where ValueError should come from. Validate the header and the row shape before adding any value to a total.
import csv, io
text='student,score\nAda,90\n'
print(list(csv.DictReader(io.StringIO(text))))
[{'student': 'Ada', 'score': '90'}]- Read a row with DictReader.
- Inspect the score type.
Validate and aggregate
Start with a deliberately broken sum over score strings. The TypeError points to the boundary between parsed text and numerical data. A robust input loop checks the exact two-column header, trims student names, parses scores, and raises ValueError with a clear message for a blank name, a missing score, a non-integer score, or an out-of-range value. DictReader may place extra cells under a None key or missing cells under a field whose value is None. Reject both. This small project should fail predictably on malformed records rather than produce an attractive but incorrect average.
The dictionary stores a sum and count per name. A duplicate student row increases the count rather than creating a second report row. This is grouping by a key, the same idea used in many data tools.
rows=[('Ada',90),('Lin',80),('Ada',80)]
totals={}
for name,score in rows:
total,count=totals.get(name,(0,0))
totals[name]=(total+score,count+1)
print(totals)
{'Ada': (170, 2), 'Lin': (80, 1)}- Convert and range-check each score.
- Maintain a sum and count for each name.
Build a deterministic report
For each valid row, keep a running total and count under the student's name. A dictionary of dictionaries is enough; no database is needed. After reading all rows, create one output dictionary per student with name, count, and average rounded to one decimal place. Sort reports by descending average and then ascending student name so ties are stable. The sort key (-average, student) states the order plainly. Returning structured data makes the function easy to test and lets a separate interface decide whether to print a table or save JSON.
The output is structured data, so it can be printed or exported later. Test two students with the same average to show that the secondary alphabetical sort is intentional rather than accidental.
report=[{'student':'Zoe','average':90.0},{'student':'Ada','average':90.0},{'student':'Lin','average':80.0}]
print([row['student'] for row in sorted(report,key=lambda row:(-row['average'],row['student']))])
['Ada', 'Zoe', 'Lin']- Compute one rounded average per student.
- Sort by descending average, then name.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Use csv.DictReader, a totals dictionary of {name: [sum, count]}, then sorted(report, key=lambda row: (-row['average'], row['student'])).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Adding score strings
totals[name] += row['score']score = int(row['score']); totals[name] += scoreWhat happens: A type error occurs or strings concatenate instead of adding scores.
Parse and validate the numeric field before arithmetic.
Sorting only by average
sorted(rows, key=lambda row: -row['average'])sorted(rows, key=lambda row: (-row['average'], row['student']))What happens: Tie order depends on input order rather than a documented rule.
The second key gives deterministic ranking.