Adding an Opening Dashboard to Your Study Log
Learning Objectives
- You can integrate the summary functions from this part into the study log as an opening dashboard.
- You can update
load_sessionsso that the loader validates dates and rejects future-dated rows at the boundary, leaving summary functions to assume valid input. - You can write a
print_summary(sessions, today=None)function that displays the full dashboard and is deterministic in tests. - You can apply the comparing-approaches skill to choose between two reasonable implementations of a summary, and document the choice.
This chapter is the project checkpoint for this part. Your task is to add a small but useful dashboard to the opening screen of your study log. When you start the program, before it asks for the next session, the program should show you what your accumulated data adds up to.
The exercise at the end of this chapter is a single deliverable. It does not provide starter code: start from your own Part 4 persistent study log project, keep its CSV file contract and save-after-append behaviour, and add the Part 5 dashboard functionality to that program.
What You Are Building
The program already shows a small summary — but at the end of each run, after sessions have been logged. It looks something like:
=== Summary ===Sessions on record: 5Total minutes: 250Longest session: 90 minutesThe change for this part is twofold. First, the summary becomes richer — per-subject summaries with counts and averages, recent daily totals, heaviest day, current streak, and longest streak. Second, the richer summary is moved to the start of each run, so that opening the program tells you about your history before you log anything new. The new opening screen looks like this:
=== Study Log ===
So far: 5 sessions, 4 hours 10 minutes
By subject: History: 1 session, 30 minutes, average 30.0 minutes Math: 3 sessions, 130 minutes, average 43.3 minutes Physics: 1 session, 90 minutes, average 90.0 minutes
Recent days: 2026-05-10: 135 minutes 2026-05-11: 90 minutes 2026-05-12: 25 minutes
Heaviest day: 2026-05-10 (135 minutes)Current streak: 3 daysLongest streak: 3 daysIf there are no sessions yet, the dashboard is replaced by a single line:
No sessions logged yet -- ready to start.The Summary Functions
The dashboard uses these summary functions, written across the previous chapters of this part — for the summaries_by_subject-function, you’ll need to add sorting to make the output nicer:
summaries_by_subject(sessions) -> list of [subject, count, total, average] rows
sessions_between(sessions, start, end) -> list of session rows
totals_by_day(sessions) -> list of [date_string, total_minutes] rows
heaviest_day(sessions) -> [date_string, total_minutes] | None
recent_daily_totals(sessions, today=None) -> list of [date_string, total_minutes] rows
distinct_days(sessions) -> list of date objects
longest_streak(sessions) -> int
current_streak(sessions, today=None) -> intThe existing function total_minutes(minutes_list) is also reused. A small helper makes the extraction explicit:
def get_minutes(sessions): """Return the minutes field from each session as a list.""" result = [] for subject, minutes, day in sessions: result.append(minutes) return resultThen total_minutes(get_minutes(sessions)) gives the overall total — with the column extraction named, not hidden inside an inline comprehension.
The work of this checkpoint is therefore to integrate the Part 5 summary ideas into your Part 4 project as one complete program. You will harden the loader, add the summary helpers, wire the opening dashboard into the main script, and test the whole thing as a unit.
Updating load_sessions
The loader read each row and converted the minutes field to an integer. That was enough when no part of the program tried to do anything with the date column. From this part onward, the dates are used — compared, subtracted, ranged — and the loader needs to take a little more responsibility for the data it produces.
Two specific responsibilities:
- Validate the date string. A row whose date column is not valid ISO format is bad data. Skip it with a message, the same way the Part 4 loader skipped rows with non-integer minutes.
- Reject future-dated rows. The program records the current date when it saves a session. A future-dated row in the file means either bad data or a system-clock disagreement; either way, it should not be loaded as if it were a valid session.
A third, smaller thing: the get_session() already stores subjects in lowercase. The loader can do the same to defend against rows that were edited by hand and ended up with mixed casing:
subject = row[0].strip().lower()Putting it together, the updated loader, with explicit comments showing what each guard is for:
import csvfrom datetime import date
def load_sessions(path): """Load sessions from CSV. Validate at the boundary; let summaries trust the result.
Returns [] for a missing file (first-run case). Skips, with a message, any row whose date or minutes column is invalid, and any row whose date is in the future. """ 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, minutes, day_string])
return sessionsThe loader still returns the same [subject, minutes, day_string] shape — summaries operate on strings, and parsing happens inside the functions that need real date objects. What changed is that the strings are now guaranteed to be parseable and not in the future, so the summary functions can call date.fromisoformat and date comparisons without defensive try/except of their own.
The print_summary Function
Before wiring the summary into the main script, factor the display into its own function. This keeps the main script readable and makes the dashboard easy to test or change.
from datetime import date, timedelta
def print_summary(sessions, today=None): """Print the opening dashboard. Handles the empty case.
`today` defaults to date.today() and is exposed as a parameter so that tests can pin down the full output, not just the streak. """ if today is None: today = date.today()
if len(sessions) == 0: print("No sessions logged yet -- ready to start.") return
# Overall total total = 0 for subject, minutes, day in sessions: total += minutes hours, mins = divmod(total, 60) print(f"So far: {len(sessions)} sessions, {hours} hours {mins} minutes") print()
# By subject print("By subject:") for subject, count, subj_total, avg in summaries_by_subject(sessions): sessions_word = "session" if count == 1 else "sessions" print(f" {subject.title()}: {count} {sessions_word}, {subj_total} minutes, average {avg:.1f} minutes") print()
# Recent days recent = recent_daily_totals(sessions, today) if len(recent) > 0: print("Recent days:") for day, day_total in recent: print(f" {day}: {day_total} minutes") print()
# Heaviest day heavy = heaviest_day(sessions) if heavy is not None: print(f"Heaviest day: {heavy[0]} ({heavy[1]} minutes)")
# Streaks print(f"Current streak: {current_streak(sessions, today)} days") print(f"Longest streak: {longest_streak(sessions)} days")A few small things to notice:
-
The function handles the empty case explicitly at the top and returns.
-
divmod(total, 60)is a Python built-in that returns both the quotient and the remainder of division at once.divmod(250, 60)returns(4, 10)— four hours and ten minutes. -
The overall total is computed with an explicit accumulator loop, not a comprehension — the loop reads clearly, and there is no benefit to compressing it.
-
The “Recent days” block is wrapped in
if len(...) > 0:so the heading does not appear when there is nothing to show. -
The function takes
today=None. The whole dashboard depends on “today” for the recent-days block and the current streak, so tests want to control it. The default ofNonefalls back todate.today()for production use.
Tests Before Wiring
Before wiring the summary into the main program, write tests against the dashboard with a fixed today. The test helper uses an ordinary loop (not a comprehension), to keep the test fixtures readable:
from datetime import date
def make_sessions(triples): """Build a list of sessions from (subject, minutes, date_string) triples.""" result = [] for triple in triples: result.append(list(triple)) return result
today = date.fromisoformat("2026-05-12")
# Empty case (visual only -- prints the empty message)print_summary([], today)
# Fixture from the chapter examplesexample = make_sessions([ ("math", 45, "2026-05-10"), ("physics", 90, "2026-05-10"), ("history", 30, "2026-05-11"), ("math", 60, "2026-05-11"), ("math", 25, "2026-05-12"),])
# Direct assertions on the underlying functionsassert summaries_by_subject(example) == [ ["history", 1, 30, 30.0], ["math", 3, 130, 43.333333333333336], ["physics", 1, 90, 90.0],]assert totals_by_day(example) == [ ["2026-05-10", 135], ["2026-05-11", 90], ["2026-05-12", 25],]assert heaviest_day(example) == ["2026-05-10", 135]assert longest_streak(example) == 3assert current_streak(example, today) == 3
# And the dashboard itself runs without errorprint_summary(example, today)
print("Dashboard tests passed.")Every test uses an explicit today value, not date.today(). This is what makes the tests deterministic. Tests that depend on the real current date are fragile — they pass today, fail tomorrow — and the fix is to make the date dependency explicit in the function signature so the test can control it.
Wiring It Into the Main Script
The change for this part is to use print_summary(sessions), which now handles both the empty and populated cases itself.
The relevant section of the main script:
SESSIONS_FILE = "sessions.csv"
print("=== Study Log ===")print()
sessions = load_sessions(SESSIONS_FILE)print_summary(sessions)print()
while True: subject, minutes = get_session() if subject is None: break
today_str = date.today().isoformat() sessions.append([subject, minutes, today_str]) save_sessions(SESSIONS_FILE, sessions)
label = classify_session(minutes) ...That is it — the “Loaded N previous sessions” message goes away and the richer dashboard takes its place. The end-of-run summary can stay as it was, or you can remove it now that the start-of-run dashboard covers the same ground; the choice is small.
The Verification Checklist
A small set of inputs to run the program against before declaring this part complete. Each one exercises a specific behaviour from one of the earlier chapters; each one should produce a sensible result.
| Case | Expected behavior |
|---|---|
| Empty log | Dashboard prints “No sessions logged yet — ready to start.” |
| One valid session | All summaries return single-item results |
| No rows match a filter | Counts and totals are 0; averages are None |
| Mixed-casing subject in saved file | Rows normalise to one category |
| Invalid date text in saved file | Loader skips the row with a message |
| Future-dated row in saved file | Loader skips it; streak summary not misled |
| Several sessions on one day | Daily total adds minutes; streak counts one day |
| Two heaviest days tied | Earliest tied day returned |
| Boundary of seven-day window | Today and six-days-ago included; seven-days-ago excluded |
| Latest session is yesterday | Current streak still alive |
| Latest session is two days ago | Current streak is 0 |
| Project row shape | All new helpers keep [subject, minutes, date] rows |
If you can reproduce each row of this table in a test or by inspection, the project for this part is complete.
Study log opening dashboard
0 / 70 points
Build the Part 5 study log dashboard as one complete deliverable.
This checkpoint builds on top of your Part 4 persistent study log project. No starter code is provided: start from your own Part 4 main.py, keep its CSV-backed study log behavior, and add the Part 5 functionality below.
Preserve the Part 4 contract: use sessions.csv, declaring it with SESSIONS_FILE = "sessions.csv", CSV header ["subject", "minutes", "date"], in-memory rows shaped [subject, minutes, date_string], date.today().isoformat() for new rows, immediate save-after-append behavior, the helper functions classify_session, total_minutes, longest_session, get_session, get_minutes, load_sessions, and save_sessions, the Part 4 response lines, and the end-of-run summary headed with === Summary ===.
Example: if sessions.csv contains this data and today is May 12, 2026:
subject,minutes,datemath,45,2026-05-10history,30,2026-05-10math,60,2026-05-11physics,90,2026-05-12then the opening dashboard should include:
=== Study Log ===
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 daysHarden load_sessions(path, today=None): 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 rows shaped [subject, minutes, date_string].
Add these Part 5 summary helpers:
| Function | Required result |
|---|---|
summaries_by_subject(sessions) | sorted [subject, count, total, average] rows |
sessions_between(sessions, start, end) | session rows 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 |
Add print_summary(sessions, today=None). It must use date.today() when today is None, accept a fixed today value for deterministic tests, print No sessions logged yet -- ready to start. for an empty session list, and otherwise print total sessions, total time in hours and minutes, by-subject summaries, recent daily totals when present, the heaviest day when present, the current streak, and the longest streak.
Wire the dashboard into the complete program: define main(), print === Study Log === at startup, load sessions before asking for input, call print_summary(sessions) before asking for a new session, remove the old Part 4 opening messages Loaded {n} previous sessions. and No previous sessions yet., keep asking for sessions until done, and keep saving immediately after every recorded session.
A Note on Using AI for This Project
The fifth skill — comparing approaches — has a natural home in this project, because almost every summary function in it admits at least two reasonable implementations. A workflow that combines all five AI skills:
- Spec. Each summary function gets a brief specification, including its empty-data behaviour and (where relevant) its tie policy. The summary-contract table from Chapter 1 is the starting point.
- Write your own version first. Even a draft. The act of writing the function clarifies what you actually want it to do — and gives you something concrete to compare against.
- Ask the AI for an alternative. “Here is my version of
heaviest_day. Can you write a different reasonable approach?” Reading both implementations side by side is where the comparing-approaches skill gets traction. - Verify both versions against the same tests. Especially the tie test, the empty test, and any boundary tests. Anything that disagrees with the spec is a bug, regardless of which version it came from.
- Choose, and document. A one-line comment in your code — “Using the explicit ‘best so far’ loop because the empty case and tie policy are visible in the code itself” — captures the decision in a way that helps the next reader.
This is the five-skill arc condensed into one project: spec, read, verify, detect, compare. The AI does the typing for half the candidates; you do the deciding.
Things to Try Beyond the Minimum
A few open extensions, none required:
- Today’s total in the dashboard. A line that says “Today: N minutes across M sessions” before the per-subject block. This needs a filter on today’s date plus a count and a sum.
- Per-subject median. Median needs a sorted list and a positional pick (the middle item, or the average of the two middle items). It is a useful summary that fits cleanly after sorting; the empty case is
None. - End-of-run delta. Keep the end-of-run summary, but only show what changed in this run — minutes added, days added, whether the streak grew. This needs you to snapshot the dashboard state at the start of the run and compare to the end.
- A streak history. Instead of just the longest streak, show every streak of two or more days. The single-pass loop in
longest_streakbecomes a single-pass loop that records each streak as it ends, rather than just tracking the maximum.
Each of these is a natural place to apply the comparing-approaches skill: try writing it yourself, ask an AI for an alternative, compare, choose.
Check Your Understanding
- Why does the loader in this part validate dates and reject future-dated rows?
- The chapter says to pass
today=...toprint_summaryin tests. Why does the full dashboard need this, not just the streak? - What is the difference between “no sessions match the filter” (which returns
0) and “no sessions at all” (which prints “No sessions logged yet”)? Where in the code do these two cases get distinguished? - The function
save_sessionsis reused unchanged. Why does it not need updating, given how much else changed? - The chapter recommends writing your own version of a summary function before asking the AI for one. What does writing your own version first give you that asking the AI first does not?
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.