Syllabus Lesson 65 of 239 · Pythonic Code, Testing & Capstone
Pythonic Code, Testing & Capstone

Capstone: the ExpenseApp

This is the finale -> your mid-tier Python certification artifact. Part 1 gave you the data layer. Now you build a real, runnable app: an ExpenseApp class you drive with text commands, the way a tiny CLI works. One method, dispatch(command), takes a string like "add Coffee 3.50 food" and returns the line to show.

This single class exercises the whole module at once -> a class holding state, a decorator that wraps a method, a generator that yields lazily, and JSON persistence so the app can save and reload. Build all of it.

The commands dispatch handles

Split the command on spaces; the first word is the verb. Return these exact strings:

  • add <desc> <amount> <category> -> records an expense, returns "Added Coffee: 3.50 (food)" (amount to two decimals).
  • total -> returns "TOTAL: 12.50" (grand total, two decimals).
  • report -> one "category: total" line per category sorted alphabetically (two decimals), then a final "TOTAL: grand_total" line, joined by newlines. An empty ledger returns "No expenses recorded."
  • budget <category> <amount> -> sets a spending limit, returns "Budget for food: 9.00".
  • over -> if any categories are over budget, returns "Over budget: food, fun" (alphabetical, comma-joined); otherwise "All categories within budget."

Store the ledger and budgets on self in __init__. Amounts arrive as strings from the command -> convert with float(...). Format with f"{value:.2f}".

The decorator: @log_calls

Write log_calls and apply it to dispatch. Each time the wrapped method runs it appends the function's name to self.call_log, then calls through. Use @functools.wraps(func) on the inner wrapper so the decorated method keeps its real __name__ (the tests check ExpenseApp.dispatch.__name__ == "dispatch"). Because the wrapper takes self first, signature is def wrapper(self, *args, **kwargs):.

The generator: iter_over_budget(self)

A real generator method -> it yields each category whose spend exceeds its budget, in alphabetical order. The hidden test asserts inspect.isgeneratorfunction(ExpenseApp.iter_over_budget), so it must use yield, not return a list. The over command should consume this generator with list(self.iter_over_budget()).

Persistence: to_json / from_json

to_json(self) returns a JSON string capturing the ledger and budgets. from_json(cls, text) is a @classmethod that builds a fresh app from that string. The round-trip must be lossless -> ExpenseApp.from_json(app.to_json()) reproduces the same ledger and budgets.

Try the AI tutor

If your tutor is on (the app wires window._floatiTutor), ask it to suggest a budgeting tip for your biggest category. Grading never touches model output -> only your deterministic command transcript is checked.

Press Run: the hidden tests build an app, fire a scripted list of commands at dispatch, and assert the exact transcript line by line -> then check the generator, the JSON round-trip, and the preserved name. Pass them and you have shipped a real, tested, mid-tier Python app.

Your turn

Build a runnable ExpenseApp class. dispatch(command) handles add <desc> <amount> <category>, total, report, budget <category> <amount>, and over, returning the exact strings in the lesson. Write a log_calls decorator (using functools.wraps) applied to dispatch that appends each call's name to self.call_log. Write iter_over_budget(self) as a GENERATOR yielding over-budget categories alphabetically. Add to_json(self) and a from_json(cls, text) classmethod so the app round-trips losslessly. Never call input() -> commands arrive only through dispatch.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output