Six days of pieces. Today they become a program.
The project below is a habit tracker: it stores entries, saves them to a file, and reports on them. It is small enough to read in one sitting and real enough to be worth finishing.
The finished program
Read it first, then run it. Every construct in here comes from a lesson this
week — dataclasses from Day 6, comprehensions from Day 4, json and exception
handling from Day 5, dictionaries from Day 3.
Why it is split this way
Three modules, three jobs, dependencies pointing one way:
tracker.pyholds the rules and knows nothing about files or printing. Every method is testable with no setup at all.storage.pyis the only module that touches the filesystem. Swap it for a database module and nothing intracker.pychanges.main.pydecides what actually happens, behind a main guard.
That separation — rules, storage, entry point — is the smallest useful version of how most real applications are laid out.
One line in storage.py uses something from an explore lesson:
Entry(**item). The ** spreads a dictionary into keyword arguments, so
{"habit": "read", "day": "2026-01-05", "minutes": 30} becomes
Entry(habit="read", day="2026-01-05", minutes=30). Arguments in Depth has
the rest of the story; that sentence is all this program needs.
Your turn
Take the program above and make it yours. In rough order of difficulty:
- Add
average_minutes(habit)and show it in the report. - Add
recordvalidation: reject adaythat is notYYYY-MM-DD.date.fromisoformatraisesValueErrorfor you — catch and re-raise with a clearer message. - Add a
remove(habit, day)method, and decide what should happen when there is no such entry. - Write tests for
streak— the empty case, a single day, a broken run, and two separate runs where the longer one comes first. - Add a command line: read commands from
input()in a loop —add read 2026-01-08 30,report,quit— parsing withsplit()and handling bad commands without crashing.
Start here: average minutes and a tested streak
Add average_minutes(habit) to Tracker and print it from main.py, then
write assertions covering the four streak cases listed above. For the entries
already in main.py, the average for "read" is 33.3, and when all four
streak assertions pass, nothing complains.
Hint 1: the idea
average_minutes needs no new data: total_minutes(habit) and
days_for(habit) already give you the two numbers an average is made of. The
only new decision is what the average of no days should be. Each streak test
is a fresh Tracker holding just the entries that make its case, and one
assert about its streak.
Hint 2: a first step
Get print(tracker.average_minutes("read")) showing 33.3 first — the entries
in main.py total 100 minutes over 3 days. Then write the easiest streak test,
the empty one: assert Tracker().streak("read") == 0. The other three each
need a new Tracker and a few record calls.
Show one solution
def average_minutes(self, habit):
"""Mean minutes per recorded day, or 0 when there are none."""
days = self.days_for(habit)
if not days:
return 0
return round(self.total_minutes(habit) / len(days), 1)And the streak assertions:
empty = Tracker()
assert empty.streak("read") == 0
one = Tracker()
one.record("read", "2026-01-05", 10)
assert one.streak("read") == 1
broken = Tracker()
for day in ["2026-01-05", "2026-01-06", "2026-01-09"]:
broken.record("read", day, 10)
assert broken.streak("read") == 2
longest_first = Tracker()
for day in ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-09"]:
longest_first.record("read", day, 10)
assert longest_first.streak("read") == 3
print("all streak tests passed")The last case is the one that catches a real bug: an implementation that
returns current instead of longest passes the first three tests and fails
this one.
Two more pages
The habit tracker was supported practice: you read a finished program and extended it. That’s valuable, and it’s not the same as starting one from a blank file. Two short pages finish the course:
- On Your Own — a fresh brief with sample output and no finished program above it. The real test of the week.
- Come Back Later — review challenges for a week from now and a month from now, because what you can still do then is what you’ve actually learned.
Where to go next
You know the language. What remains is choosing a direction and building things in it. There’s no single “next step”: pick by what you want to make.
| If you want to… | Learn next |
|---|---|
| Analyse data | pandas, numpy, Jupyter notebooks |
| Build web APIs | FastAPI or Django, plus HTTP basics |
| Automate your machine | pathlib, subprocess, argparse, scheduling |
| Scrape and call APIs | requests, httpx, BeautifulSoup |
| Write better Python | pytest, ruff, mypy, and the itertools docs |
| Understand the language deeply | Fluent Python, and the Python source itself |
Two of those directions have courses here. Writing Reliable Python (Intermediate) is for turning scripts into tools that survive messy real-world input: tests, parsing, storage and a command line. Designing Reliable Libraries (Advanced) is narrower — it’s for people who write code that other developers call, and it’s one destination among the ones in the table, not the step everyone should take next. If you want to analyse data or build a web app, the libraries in the table are the better next move.
Three habits matter more than any of those choices:
Build something you actually want. Motivation beats curriculum. A script that renames your photos will teach you more than ten tutorials.
Read other people’s code. The standard library is on your machine and much
of it is readable. import random; print(random.__file__) will show you where.
Write it down when it breaks. Every error you understand is one you will never lose an hour to again.
The week, in one page
| Day | What you can now do |
|---|---|
| 1 | Variables, numbers, strings, f-strings, input and conversion |
| 2 | Conditions, while and for, break and continue |
| 3 | Lists, tuples, dictionaries, sets — and choosing between them |
| 4 | Functions, arguments, scope, lambdas, comprehensions |
| 5 | Exceptions, files, the standard library, your own environment |
| 6 | Classes, dunder methods, inheritance, dataclasses, type hints |
| 7 | Generators, packages, tests, and a program of your own |
That is the essential language. Everything else in Python is a library on top of what you have just learned.