Summarizing Your Data

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_sessions so 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: 5
Total minutes: 250
Longest session: 90 minutes

The 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 days
Longest streak: 3 days

If 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) -> int

The 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 result

Then 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:

  1. 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.
  2. 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 csv
from 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 sessions

The 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 of None falls back to date.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 examples
example = 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 functions
assert 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) == 3
assert current_streak(example, today) == 3
# And the dashboard itself runs without error
print_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.

CaseExpected behavior
Empty logDashboard prints “No sessions logged yet — ready to start.”
One valid sessionAll summaries return single-item results
No rows match a filterCounts and totals are 0; averages are None
Mixed-casing subject in saved fileRows normalise to one category
Invalid date text in saved fileLoader skips the row with a message
Future-dated row in saved fileLoader skips it; streak summary not misled
Several sessions on one dayDaily total adds minutes; streak counts one day
Two heaviest days tiedEarliest tied day returned
Boundary of seven-day windowToday and six-days-ago included; seven-days-ago excluded
Latest session is yesterdayCurrent streak still alive
Latest session is two days agoCurrent streak is 0
Project row shapeAll 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,date
math,45,2026-05-10
history,30,2026-05-10
math,60,2026-05-11
physics,90,2026-05-12

then 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 days
Longest streak: 3 days

Harden 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:

FunctionRequired 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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_streak becomes 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

  1. Why does the loader in this part validate dates and reject future-dated rows?
  2. The chapter says to pass today=... to print_summary in tests. Why does the full dashboard need this, not just the streak?
  3. 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?
  4. The function save_sessions is reused unchanged. Why does it not need updating, given how much else changed?
  5. 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.

Diagrams tried: 0