From Rows to Records
Learning Objectives
- You can convert the study log’s sessions from a list of rows to a list of dictionaries.
- You can update the loader, summaries, and dashboard to use named-field access.
- You can carry out a representation change as a behaviour-preserving refactor, checking with your existing tests.
- You can explain why named access makes the code easier to read and harder to get wrong.
The previous chapters built up dictionaries in the abstract. This chapter and the next put them to work, by migrating the study log from list-of-rows to list-of-dicts and then to JSON storage.
This is the first of the part’s two project migrations. It is a refactor that changes the structure but leaves behaviour untouched: the program will load the same sessions, print the same dashboard, and save the same data. What changes is how a session is represented in memory — and therefore how every function that touches one is written.
The Change: From List of Rows to List of Records
Right now a session is a list of three fields in a fixed order:
["math", 45, "2024-03-10"]After this chapter it is a dictionary with three named fields:
{"subject": "math", "minutes": 45, "date": "2024-03-10"}The whole migration is a consequence of that one decision. Everywhere the old code said row[0], the new code says session["subject"]; everywhere it said row[1], the new code says session["minutes"]; everywhere it said row[2], the new code says session["date"].
Replacing a row index
0 / 5 points
A session used to be stored as a row like ["math", 45, "2026-04-01"].
It is now stored as a dictionary like {"subject": "math", "minutes": 45, "date": "2026-04-01"}.
Which expression is the named-field replacement for row[1]?
Refactoring Code
Changing a representation that the entire program depends on is a big change. The safest option is refactoring the code: changing one piece at a time, and after each change, run the tests to confirm behaviour did not change.
The tests you wrote in the Summarizing Your Data part are what make this possible. They check what totals_by_subject, current_streak, and the rest produce, not how they are written inside. As long as you update those tests’ fixtures to the new session shape, they will tell you whether each rewritten function still behaves.
So the first step is to update one fixture and see what a test looks like in the new shape:
example = [ {"subject": "math", "minutes": 45, "date": "2024-03-10"}, {"subject": "physics", "minutes": 90, "date": "2024-03-10"}, {"subject": "history", "minutes": 30, "date": "2024-03-11"}, {"subject": "math", "minutes": 60, "date": "2024-03-11"}, {"subject": "math", "minutes": 25, "date": "2024-03-12"},]The assertions stay the same, because the answers do not change — math still totals 130 minutes whether a session is a list or a dictionary.
Updating a Summary Function
Take total_minutes_for_subject from the chapter Applying Summary Patterns to Rows. The list version unpacked the row in the loop header:
def total_minutes_for_subject(sessions, wanted_subject): total = 0 for subject, minutes, day in sessions: if subject == wanted_subject: total += minutes return totalThe dictionary version takes each session whole and reaches in by name:
def total_minutes_for_subject(sessions, wanted_subject): total = 0 for session in sessions: if session["subject"] == wanted_subject: total += session["minutes"] return totalThe loop header changes from unpacking three positions to taking one session. Inside, subject becomes session["subject"] and minutes becomes session["minutes"]. The logic — filter by subject, total the minutes — is identical, and so is the result.
The function totals_by_subject reads each session’s fields by name:
def totals_by_subject(sessions): totals = {} for session in sessions: subject = session["subject"] totals[subject] = totals.get(subject, 0) + session["minutes"] return totalsPulling session["subject"] into a local variable named subject keeps the dictionary-accumulation line readable. You could write session["subject"] twice instead; the local is clearer.
Total minutes from records
0 / 5 points
Arrange the lines into a function that totals minutes for one event name.
The events argument is a list of dictionaries. Each dictionary has "name", "minutes", and "date" keys.
Updating the Loader
The Making Your Study Log Persistent from part 4 reads each CSV row, validated it, and appended a list. The new loader does the same validation but appends a dictionary:
import csvfrom datetime import date
def load_sessions(path): """Load sessions from CSV as a list of dictionaries.
Returns [] for a missing file. Skips rows with bad data or future dates. """ sessions = [] today = date.today() try: f = open(path, encoding="utf-8") except FileNotFoundError: return sessions
with f: reader = csv.reader(f) try: next(reader) # skip header except StopIteration: return sessions
for row in reader: if len(row) != 3: print(f"Skipping malformed row: {row}") continue subject = row[0].strip().lower() try: minutes = int(row[1].strip()) except ValueError: print(f"Skipping row with bad minutes: {row}") continue day_string = row[2].strip() try: day = date.fromisoformat(day_string) except ValueError: print(f"Skipping row with bad date: {row}") continue if day > today: print(f"Skipping future-dated row: {row}") continue sessions.append({ "subject": subject, "minutes": minutes, "date": day_string, })
return sessionsOnly the last statement changed. Where the old loader did sessions.append([subject, minutes, day_string]), this one builds a dictionary with named fields. Everything else — reading the CSV, skipping the header, validating each field, rejecting future dates — is exactly as it was. The file on disk is still CSV; only the in-memory shape is different.
You’ll change the file format in the next chapter.
Notice that the loader is the one place that creates sessions from raw data, so it is the natural place to fix the field names. Once the loader produces dictionaries, every function downstream receives dictionaries, and the migration ripples outward from this single point.
Updating the Dashboard
The dashboard’s print_summary reaches into sessions in a couple of places — to total the overall minutes, and to print the per-subject and per-day blocks. The overall-total loop changes the same way every other loop did:
total = 0for session in sessions: total += session["minutes"]The per-subject block calls summaries_by_subject, which already returns its result keyed by subject, so the printing loop iterates the summary dictionary with .items(). The exact shape of that loop depends on what summaries_by_subject returns — you will settle it when you do the project — but the access pattern is the named one throughout.
Study log dashboard from records
0 / 50 points
Return to your Part 5 study log opening dashboard and refactor it from rows to records.
This checkpoint builds on top of the exercise Study log opening dashboard from the end of the last part. No starter code is provided: start from your own Part 5 main.py and keep the user-facing behaviour familiar. The file on disk is still sessions.csv; the change is the in-memory shape of each session.
In Part 5, a session row looked like this:
["math", 45, "2026-05-10"]In this exercise, every loaded or newly created session must be a dictionary:
{"subject": "math", "minutes": 45, "date": "2026-05-10"}Preserve the CSV contract: use sessions.csv, keep the CSV header ["subject", "minutes", "date"], use the csv module, open files with encoding="utf-8", use date.today().isoformat() for new sessions, and save immediately after appending each new session.
Required helpers:
| Function | Required result |
|---|---|
classify_session(minutes) | "short" below 15, "solid" from 15 through 90, "long" above 90 |
total_minutes(minutes_list) | total minutes, or 0 for an empty list |
longest_session(minutes_list) | largest minute value, or None for an empty list |
get_session() | ask for subject and minutes, returning (None, None) for done |
get_minutes(sessions) | extract the "minutes" values from session dictionaries |
load_sessions(path, today=None) | load CSV rows into session dictionaries |
save_sessions(path, sessions) | write session dictionaries back to CSV |
summaries_by_subject(sessions) | sorted [subject, count, total, average] rows |
sessions_between(sessions, start, end) | original session dictionaries inside the inclusive date window |
totals_by_day(sessions) | sorted [date_string, total_minutes] rows |
heaviest_day(sessions) | earliest [date_string, total_minutes] row with the largest total, or None |
recent_daily_totals(sessions, today=None) | totals for today and the previous six days |
distinct_days(sessions) | sorted distinct date objects |
longest_streak(sessions) | longest run of consecutive session days, or 0 |
current_streak(sessions, today=None) | current streak; yesterday still counts, old streaks return 0, future dates raise ValueError |
print_summary(sessions, today=None) | print the opening dashboard using session dictionaries |
main() | run the complete study log program |
Harden load_sessions(path, today=None) as in the Part 5 checkpoint: use date.today() when today is None; return [] for missing, empty, or header-only files; strip fields; normalize subjects with .lower(); parse minutes as int; validate dates with date.fromisoformat(...); skip malformed rows, bad-minute rows, bad-date rows, and rows dated after today; print a short skipped-row message containing malformed, bad minutes, bad date, or future-dated; and keep valid sessions shaped as dictionaries.
The opening dashboard output should stay the same as in Part 5. For these session records and today set to May 12, 2026:
sessions = [ {"subject": "math", "minutes": 45, "date": "2026-05-10"}, {"subject": "history", "minutes": 30, "date": "2026-05-10"}, {"subject": "math", "minutes": 60, "date": "2026-05-11"}, {"subject": "physics", "minutes": 90, "date": "2026-05-12"},]print_summary(sessions, today=date(2026, 5, 12)) should print:
So far: 4 sessions, 3 hours 45 minutes
By subject: History: 1 session, 30 minutes, average 30.0 minutes Math: 2 sessions, 105 minutes, average 52.5 minutes Physics: 1 session, 90 minutes, average 90.0 minutes
Recent days: 2026-05-10: 75 minutes 2026-05-11: 60 minutes 2026-05-12: 90 minutes
Heaviest day: 2026-05-12 (90 minutes)Current streak: 3 daysLongest streak: 3 daysComplete program behavior:
- define
SESSIONS_FILE = "sessions.csv", - do not ask for input when the file is imported,
- guard the input loop with
if __name__ == "__main__": main(), - print
=== Study Log ===at startup, - load existing CSV sessions before asking for input,
- call
print_summary(sessions)before asking for a new session, - keep asking for subjects until the user types
done, - accept variants such as
Done,DONE, anddone, - ask for minutes only after a non-quit subject,
- re-prompt with
Please enter a whole number of minutes.after non-numeric minutes, - create a new session dictionary for each recorded session,
- save immediately after every successfully recorded session,
- keep the Part 5 response lines for short, solid, and long sessions,
- and print the end-of-run
=== Summary ===.
Use dictionaries for sessions throughout the program after loading.
Check Your Understanding
- What is the mechanical substitution that turns the list-of-rows version of a summary function into the list-of-dicts version?
- Why does the chapter call the loader “the natural place to fix the field names”?
- The chapter calls this migration a refactor. What does that say about the program’s behaviour before and after?
- How do the tests you wrote in the previous part help you carry out this migration safely?
- Named access turns a class of silent bugs into something more visible. Into what, specifically?
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.