Skip to content

Module 1: Programming and debugging

Build a small command-line task tracker. The application is only a vehicle: the real goal is to practise turning requirements into behavior, separating logic from side effects, diagnosing failures, and changing code safely.

Learning outcomes

By the end of this module, you should be able to:

  • translate a small requirement into functions and data;
  • validate input and report useful errors;
  • persist data without mixing storage logic into every command;
  • reproduce, isolate, and fix a defect;
  • write tests that protect observable behavior;
  • use Git commits to preserve understandable checkpoints.

Build contract

Use Python and its standard library. Build four commands:

tasks add "Write module one"
tasks list
tasks done 1
tasks delete 1

Each task needs only three fields:

Field Rule
id Unique positive integer
title Non-empty text after surrounding whitespace is removed
done Boolean, initially false

Store the collection in a JSON file. Keep the filename configurable so tests can use a temporary directory.

Do not add a database, web framework, dependency-injection container, or packaging system. None is needed to meet this module's outcomes.

Mental model

A useful first decomposition is:

CLI arguments → validation → task operations → persistence → JSON file
                       ↓
                  result/error

The boundaries matter more than the number of files:

  • CLI boundary: converts strings into an operation and presents its result.
  • Domain logic: adds, completes, deletes, and lists tasks without printing or reading files.
  • Persistence boundary: loads and saves the task collection.

Keeping domain logic free of terminal and filesystem access makes failures easier to locate and tests easier to write. Start with one file if that remains readable; split it only when the boundaries become difficult to see.

Work in four checkpoints

1. Model the behavior

Implement operations for adding, listing, completing, and deleting tasks. Decide what happens when:

  • a title is empty;
  • an ID does not exist;
  • the same task is completed twice;
  • the collection is empty.

Return values or raise specific exceptions. Do not print inside these operations.

Check your understanding:

Which rules belong to the task tracker, and which belong only to its command-line interface?

2. Add persistence

Implement one function that loads tasks and one that saves them. A missing file should produce an empty collection. Malformed JSON should produce a clear error rather than silently discarding data.

Write to a temporary file and replace the destination after serialization succeeds. This prevents a failed write from leaving a partially written data file.

Check your understanding:

Why is “missing file” a valid initial state while “malformed file” is an error?

3. Connect the CLI

Use argparse to define the four commands. Keep this layer thin:

  1. parse arguments;
  2. load tasks;
  3. call one operation;
  4. save only after a successful mutation;
  5. print the result or a concise error.

Use a non-zero exit status for invalid input and failed operations. Test the program manually from a directory that does not yet contain its data file.

4. Protect the behavior

Use unittest and tempfile; no testing dependency is necessary. At minimum, cover:

  • adding a valid task;
  • rejecting a blank title;
  • completing an existing task;
  • rejecting an unknown ID;
  • loading when the file is absent;
  • rejecting malformed stored data;
  • saving and loading a round trip.

Prefer tests of observable behavior. A test should remain valid if you reorganize private helper functions without changing what the application does.

Debugging exercise

Introduce one defect deliberately—for example, calculate the next ID from the number of tasks. Then delete a task and add another. The new task may reuse an existing ID.

Use this loop:

  1. Reproduce: record the shortest command sequence that exposes the defect.
  2. Minimize: remove any step that is not required.
  3. Inspect: observe the task collection before and after the failing operation.
  4. Hypothesize: state which rule the code violates.
  5. Test: add a failing regression test.
  6. Fix: make the smallest change that restores the rule.
  7. Verify: run the entire test suite, not only the new test.

The likely invariant is more useful than the symptom: every stored task ID must remain unique.

Git checkpoints

Commit working states that explain the progression:

  1. task operations and their tests;
  2. JSON persistence and failure handling;
  3. command-line interface;
  4. regression test and ID fix;
  5. setup and usage documentation.

Before each commit, inspect git diff, run the tests, and write a message describing the behavior introduced. Avoid committing generated files, local data, virtual environments, or secrets.

Evidence to keep

Your repository should contain:

  • application code;
  • automated tests;
  • a README with setup, test, and usage commands;
  • an ignore rule for local task data;
  • a short debugging note containing the reproduction, violated invariant, and fix.

Someone using a clean checkout should be able to run the tests and use the CLI by following only the README.

Completion check

You are ready for the next module when you can demonstrate all of the following:

  • every command has a success and a failure example;
  • tests run without reading or overwriting your real task file;
  • malformed stored data is reported and preserved;
  • the duplicate-ID regression test fails against the defective implementation and passes after the fix;
  • git log shows small, understandable checkpoints;
  • you can explain the CLI, domain, and persistence boundaries without referring to file names.

Read only when needed

Continue with Module 2: How programs run after the completion check passes.