Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 10 of 10

Time to build something with a safety net. This project is a command-line to-do manager that saves to JSON — and, more importantly, a test suite that proves it works.

You already built projects that store data (Module 6's expense tracker). The difference here is that when you change this one, the tests tell you instantly whether you broke it.

What we're building

  • Add tasks with a title and a priority
  • Mark tasks complete, remove them, list them
  • Save to and load from a JSON file, surviving a corrupt file
  • A unittest suite covering the happy path, the edges, and the errors

Step 1: the data model

Everything the app can do lives in one class. No printing, no input() — just behaviour that returns values and raises errors. That separation is exactly what makes it testable.

Three design choices worth naming:

  • _next_id is saved too. Without it, reloading and adding a task would reuse an old id. There's a test for exactly this below.
  • A missing file loads as empty; a corrupt file raises. "No tasks yet" is normal. "Your data is unreadable" is not — silently starting fresh would destroy someone's list.
  • One custom exception, TaskError. Callers catch one thing, whatever went wrong.

Step 2: the tests

Now the part that makes this a Module 9 project. In a real repo the class lives in todo.py and the tests start with from todo import TodoList, TaskError — here we paste the class in so the block runs standalone.

setUp gives every test a fresh temp directory, so no test can see another's file.

Twelve tests, grouped by what they're for: the happy path, the error paths, persistence. Notice what's not tested — json.dump works, Path.exists works. Test your logic, not the standard library.

Break something on purpose: change self._next_id += 1 to self._next_id += 2 and re-run. test_add_assigns_incrementing_ids and test_ids_keep_counting_after_reload go red and name themselves. That's the whole payoff.

Step 3: the CLI

The interface layer is thin on purpose — read input, call a method, print the result, catch TaskError. All the thinking already happened in the class.

Try it: add a couple of tasks, complete one, list them, then save and quit. Ask for id 99 and watch it print an error instead of crashing.

Note main() never lets an exception escape — it catches TaskError from the list and ValueError from int(). A CLI that shows a traceback to a user is a CLI with a bug.

Take it further

Each of these is a change plus the test that proves it:

  1. Due dates. Store an ISO date string, add overdue() using datetime (lesson 9.6). Test it with a fixed date, never date.today() — a test that changes answer by the day is worse than no test.
  2. Search. find_by_text(query), case-insensitive. Test: a match, a non-match, mixed case.
  3. Sorting. sorted(tasks, key=...) by priority then id (lesson 4.7).
  4. Argparse. Swap the menu for real arguments — todo add "buy milk" --priority high. The argparse module is in the standard library.
  5. Atomic saves. Write to todo.json.tmp then os.replace it (lesson 8.7), so a crash mid-write can't corrupt the file.

Module 9 complete

You can now write comprehensions that stay readable, generators that stream instead of materialise, decorators that wrap behaviour, and context managers that clean up after themselves. You handle dates without off-by-one bugs, pull structure out of text with regex, write code other Python programmers recognise on sight — and prove it works with tests.

That's the intermediate arc. Next up: Module 10 — Capstone Projects, where you pick something real and ship it.

Questions about this lesson

Log in to ask a question.