The Persistent Tracker

Intermediate · 90 min build · 🏆 milestone project

You are ready for the first program in this course that behaves like a small tool, not a one-off script. The goal is a persistent expense tracker, a command-line program that stores expense records in a JSON file during the page session, loads them back, and handles bad input without crashing.

The browser filesystem is temporary: a page reload clears it. Build your save and load tests inside one run, and think of the file as a practice notebook for this session, not a permanent vault.

What you'll build

You will build one menu-driven expense tracker with these routes:

  • 1: add an expense with a date, category, amount, and note.
  • 2: list all expenses and show the total.
  • 3: save expenses to expenses.json.
  • 4: load expenses from expenses.json.
  • 5: add demo expenses so you can test without typing a lot.
  • 7: exit cleanly.

An expense record is one dictionary that describes one purchase. A JSON data file is a text file that stores lists and dictionaries in a shape Python can read back with json.load().

The scaffold is starter code with TODO comments that show where your work belongs. The acceptance criteria are checks you can run yourself to prove each stage works.

python — playgroundlive
⌘/Ctrl + Enter to run

Checkpoint 1: the menu and data shape

Start with the front desk of the program: the menu. Keep the top-level route code small, and let functions do the work behind each choice.

Acceptance criteria:

  • Entering 7 prints Goodbye and stops.
  • Entering 1, 2, 3, 4, or 5 reaches the matching function.
  • Entering another value prints a clear fallback message.
  • Expenses are stored as a list of dictionaries.
  • Each expense dictionary uses the same keys: date, category, amount, and note.

Checkpoint 2: add and list expenses

Build the in-memory tracker before you touch files. In-memory means the data lives in Python values while the program is running, like name tags attached to objects in memory.

Acceptance criteria:

  • Choice 1 asks for date, category, amount, and note.
  • A valid amount becomes a float, not a string.
  • A non-numeric amount prints a friendly message instead of a traceback.
  • Choice 2 prints a useful empty-state message when the list is empty.
  • Choice 2 lists every expense and prints the total with two decimal places.

One practical tip: this playground allows up to 20 typed answers per Run, and every manual add costs five of them (the menu choice plus four fields). When you test repeatedly, lean on route 5 — that is exactly what the demo data is for.

Checkpoint 3: save and load JSON

Now connect the tracker to a file. Use JSON because the data is already a list of dictionaries; it fits the shape without flattening.

Acceptance criteria:

  • In one run, choosing 5, then 3, then 4, then 7 proves save and load without depending on a previous Run.
  • save_expenses() writes expenses.json with encoding="utf-8" and indent=2.
  • load_expenses() returns a list loaded with json.load().
  • A missing file prints a friendly message and returns an empty list.
  • Broken JSON catches json.JSONDecodeError and returns an empty list.

Do not promise the file survives a page reload. In this browser, the file is for practice during the current page session.

Checkpoint 4: bad input and recovery

This is the robustness checkpoint. Your program should expect ordinary human mistakes: a blank note, a mistyped amount, a missing file, and a menu choice that is not on the list.

Acceptance criteria:

  • The program catches ValueError around amount conversion.
  • It catches FileNotFoundError around loading.
  • It catches json.JSONDecodeError around loading broken JSON.
  • It does not use except Exception: as the main strategy.
  • The menu keeps messages specific: "amount must be a number" is better than "something went wrong."

Stretch goals

Choose one or two after the base tracker works:

  • Add a 6 route that shows totals by category using a dictionary.
  • Add a delete route that removes one expense by number.
  • Add a CSV export route using csv.DictWriter.
  • Sort expenses by date before listing them.
  • Add a small assert inside a helper to check that every expense has the required keys.

How to get unstuck

If the menu route goes wrong, revisit lesson 4.1 and the menu pattern from the first project. If your list or dictionary shape gets messy, go back to lesson 6.1. If a function prints when it should return, compare it to lesson 7.1. If file code fails, rebuild the write-then-read pattern from lesson 9.1, then add JSON from lesson 9.2. If your program crashes on bad input, narrow the try block and catch the specific exception from lesson 9.3.

Part II ends when this tracker feels like a set of cooperating pieces: functions, collections, files, and exceptions. Part III turns that same kind of program inside out and gives you a new way to model it with objects.

+50 XP on completion