Matheion

MA-PY1 · Module 3.2

Putting it together: a small program

Topics: Decomposition · Combining lists/dicts/classes · Program design

Overview

Module 3.2 — Putting it together: a small program

Try the assignment first — build StudyLog yourself in a .py file or REPL using only what you learned in Units 1–3. The Core ideas tab states the requirements and exact inputs/outputs. When you get stuck, open Worked snippets — that is the step-by-step tutorial, not something to copy before you try.

Then scroll Practice → Quiz to check your understanding.

Concepts

Your assignment — build StudyLog

Before reading the tutorial, try this yourself. Open a Python file (or REPL) and implement a StudyLog class that records how many minutes you studied each day.

Each log entry is one (day, minutes) tuple — for example ("Mon", 40) means 40 minutes on Monday. The log grows as you call add.

When you are stuck, scroll to Worked snippets. That tab walks through one complete solution step by step. The quiz assumes you attempted the assignment (or studied the tutorial carefully).


Required class: StudyLog

| Method | What it must do | |--------|-----------------| | __init__(self) | Start with an empty log (no entries yet). | | add(self, day, minutes) | Append one entry (day, minutes). Assume minutes is a non‑negative integer. | | total(self) | Return the sum of all logged minutes. If the log is empty, return 0. | | average(self) | Return total() / number_of_entries as a float. If the log is empty, return 0.0. | | best_day(self) | Return the (day, minutes) entry with the largest minutes. If two entries tie, keep the earlier one (the one added first). If the log is empty, return None. | | summary(self) | Return one human‑readable str (see exact formats below). |

Storage rule: each StudyLog object must have its own list of entries. Two logs must not share the same list.


Exact output formats

summary() when the log is empty:

No study logged yet.

summary() when there is at least one entry:

{n} days, {total} min total, {avg} min/day avg, best: {day} ({minutes} min)
  • {n} — number of entries (same as len(log.entries) if you store them in self.entries).
  • {total} — integer from total().
  • {avg} — average() rounded to zero decimal places (use f"{self.average():.0f}" or equivalent).
  • {day} and {minutes} — from the best_day() tuple.

Example (non-empty):

4 days, 175 min total, 44 min/day avg, best: Wed (60 min)

Test cases — inputs and expected outputs

Run these after your class is written. Your results should match exactly (except floating‑point display of average() before rounding — see notes).

Test A — empty log

log = StudyLog()
print(log.total())
print(log.average())
print(log.best_day())
print(log.summary())

| Call | Expected output | |------|-----------------| | log.total() | 0 | | log.average() | 0.0 | | log.best_day() | None | | log.summary() | No study logged yet. |

Test B — three entries

log = StudyLog()
log.add("Mon", 40)
log.add("Tue", 25)
log.add("Wed", 60)
print(log.total())
print(log.average())
print(log.best_day())
print(log.summary())

| Call | Expected output | |------|-----------------| | log.total() | 125 | | log.average() | 41.666666666666664 (125 ÷ 3) | | log.best_day() | ('Wed', 60) | | log.summary() | 3 days, 125 min total, 42 min/day avg, best: Wed (60 min) |

Note: 125 / 3 is 41.666…, but summary() prints 42 because it rounds the average to a whole number.

Test C — tie for best day

log = StudyLog()
log.add("Mon", 30)
log.add("Tue", 30)
print(log.best_day())

| Call | Expected output | |------|-----------------| | log.best_day() | ('Mon', 30) |

Tuesday also has 30 minutes, but Monday was added first, so Monday wins the tie.

Test D — driver loop (full week snippet)

log = StudyLog()
for day, minutes in [("Mon", 40), ("Tue", 25), ("Wed", 60), ("Thu", 50)]:
    log.add(day, minutes)
print(log.summary())

| Call | Expected output | |------|-----------------| | log.summary() | 4 days, 175 min total, 44 min/day avg, best: Wed (60 min) |


Optional stretch (not required for the quiz)

  • Write total, average, and best_day first as standalone functions that take a list, then move them into the class as methods.
  • Add days_logged(self) that returns len(self.entries).

Core ideas — how to decompose the assignment

If the blank page feels large, use this four‑question recipe. It maps directly onto the tools from Units 1–3.

1. What is the data, and what shape holds it? Many records of the same kind → a list. Each record is a fixed pair of fields → a tuple inside that list: list[tuple].

2. What are the small, nameable operations? total, average, best_day — each should do one job and return a value (no printing inside the calculator methods).

3. Is there state and behaviour that belong together? The same entries list feeds every operation → bundle list + methods into StudyLog, with add as the only way to append.

4. What drives it? A short loop (Test D) or a few add calls (Tests A–C) plus print(log.summary()).

Why a class instead of loose functions?

You could pass a list into standalone functions. For this assignment, the class keeps one list per log and forces updates through add, which matches how you will structure slightly larger programs later.

Read your program back

When you think you are done, say out loud:

  • what shape is my data, and who is allowed to change it?
  • what does each method take and return?
  • do Tests A–D all pass?

If any answer is fuzzy, that is where a bug is hiding — open the tutorial tab and compare your seams to the worked solution.

Self-study checklist

  • Implement StudyLog and pass Tests A–D before opening the tutorial.
  • Each report method returns its result; only the driver prints.
  • Explain why self.entries = [] belongs in __init__, not in the class body.

Examples

Tutorial — one way to solve the assignment

Use this tab when you are stuck on the StudyLog assignment in Core ideas. It follows the same four-step recipe and produces a program that passes Tests A–D.

Goal: keep a weekly study log. We record (day, minutes) entries, then report total time, average per logged day, and the best day. The assignment spec lists the exact method names and expected outputs.

Step 1 — choose the data shape

Each entry is a fixed record of two fields, and entries accumulate over the week. From the previous module that is a list of tuples:

entries = [
    ("Mon", 40),
    ("Tue", 25),
    ("Wed", 60),
]

Step 2 — write small, single-job functions

Each report is one function that takes the entries and returns a value — no printing inside:

def total_minutes(entries):
    total = 0
    for _, minutes in entries:
        total += minutes
    return total

def average_minutes(entries):
    if not entries:           # empty list is falsy
        return 0.0
    return total_minutes(entries) / len(entries)

def best_day(entries):
    best = entries[0]
    for entry in entries:
        if entry[1] > best[1]:
            best = entry
    return best               # returns the whole (day, minutes) tuple

Notice average_minutes reuses total_minutes rather than re-summing — small functions compose. And best_day carries the accumulator pattern, but the "best so far" is a whole record.

Quick check:

print(total_minutes(entries))     # 125
print(average_minutes(entries))   # 41.666666666666664
print(best_day(entries))          # ('Wed', 60)

Step 3 — bundle the state and behaviour into a class

The functions above all revolve around the same entries list. That is the cue to make a class: the list becomes instance state, and the functions become methods that read it. The class also guards how entries are added, so the data stays consistent.

class StudyLog:
    def __init__(self):
        self.entries = []          # each object gets its OWN fresh list

    def add(self, day, minutes):
        self.entries.append((day, minutes))

    def total(self):
        return sum(m for _, m in self.entries)

    def average(self):
        if not self.entries:
            return 0.0
        return self.total() / len(self.entries)

    def best_day(self):
        if not self.entries:
            return None
        best = self.entries[0]
        for entry in self.entries:
            if entry[1] > best[1]:
                best = entry
        return best

    def summary(self):
        if not self.entries:
            return "No study logged yet."
        day, minutes = self.best_day()
        return (
            f"{len(self.entries)} days, {self.total()} min total, "
            f"{self.average():.0f} min/day avg, best: {day} ({minutes} min)"
        )

Two design points worth pausing on:

  • self.entries = [] lives in __init__, so every StudyLog gets its own list. (Putting entries = [] in the class body instead would share one list across all logs — the trap from the objects module.)
  • The only way to add data is add(...). Callers never touch self.entries directly, so the object stays in a valid state.

Step 4 — the driver loop ties it together

This matches Test D from the assignment:

log = StudyLog()
for day, minutes in [("Mon", 40), ("Tue", 25), ("Wed", 60), ("Thu", 50)]:
    log.add(day, minutes)

print(log.summary())
# 4 days, 175 min total, 44 min/day avg, best: Wed (60 min)

Quick checks for the other assignment tests:

empty = StudyLog()
print(empty.summary())  # No study logged yet.

log = StudyLog()
log.add("Mon", 30)
log.add("Tue", 30)
print(log.best_day())   # ('Mon', 30)  — Test C tie rule

Read it back

Three sentences: the data is a list of (day, minutes) tuples held inside a StudyLog; the operations are total, average, and best_day, each doing one job; the driver loop adds entries and prints a summary. If you can say that about a program, you understand it — and you have just combined nearly everything from Units 1–3 into one coherent piece.

Practice check — putting it together

Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.

  1. 1.In the `StudyLog` class, why is `self.entries = []` placed inside `__init__` rather than written as `entries = []` in the class body?

  2. 2.`average()` calls `total()` instead of summing the minutes again. What is the main benefit?

  3. 3.`best_day` returns `best` where `best` started as `entries[0]` and is replaced only when `entry[1] > best[1]`. What does it return?

  4. 4.Following the recipe, what should you decide FIRST when starting a small program?

  5. 5.The report functions `return` their results instead of printing inside. Why does that matter?

  6. 6.When does the recipe say to bundle data and operations into a class rather than keep loose functions?