Mini Project: Personal Expense Tracker
Objectives
By the end of this chapter, you should be able to:
- Persist a program’s data to disk between separate runs, not just within one session
- Combine a class, JSON persistence, CSV export, and
pathlibin one working program
💡 Why this matters: Every program up to this module has lost all its data the moment it stopped running. This is the first project where what you build on Monday is still there on Tuesday, which is what makes a script feel like a real, usable tool instead of a toy.
The Project
Build a Personal Expense Tracker: a program that records expenses (amount, category, description), saves them to a JSON file so they persist between runs, and can summarize spending by category or export everything to CSV.
Core Requirements
Expense
- A class (or
@dataclass, from Module 6) withamount,category, anddescription - A
to_dict()method returning a plain dictionary: the shape you’ll actually save to JSON
ExpenseTracker
add_expense(expense): appends a newExpenseto the tracker’s collectiontotal(): returns the sum of every expense’samounttotal_by_category(): returns a dictionary mapping each category to its total spentsave_to_file(filename): writes every expense to a JSON file usingjson.dump(..., indent=2)load_from_file(filename): reads a JSON file back into a list ofExpenseobjects; if the file doesn’t exist yet, start with an empty tracker instead of raising an errorexport_to_csv(filename): writes every expense out withamount,category, anddescriptioncolumns usingcsv.DictWriter
Path Handling
Use pathlib.Path (not a bare filename string) to build the path to your data file, and check .exists() before trying to load it, rather than relying only on a try/except.
Suggested Structure
import json
import csv
from pathlib import Path
class Expense:
def __init__(self, amount, category, description):
self.amount = amount
self.category = category
self.description = description
def to_dict(self):
return {"amount": self.amount, "category": self.category, "description": self.description}
class ExpenseTracker:
def __init__(self):
self.expenses = []
def add_expense(self, expense):
self.expenses.append(expense)
def total(self):
return sum(e.amount for e in self.expenses)
def total_by_category(self):
totals = {}
for e in self.expenses:
totals[e.category] = totals.get(e.category, 0) + e.amount
return totals
def save_to_file(self, filename):
path = Path(filename)
with open(path, "w") as f:
json.dump([e.to_dict() for e in self.expenses], f, indent=2)
def load_from_file(self, filename):
path = Path(filename)
if not path.exists():
self.expenses = []
return
with open(path) as f:
data = json.load(f)
self.expenses = [Expense(**entry) for entry in data]
def export_to_csv(self, filename):
with open(Path(filename), "w") as f:
writer = csv.DictWriter(f, fieldnames=["amount", "category", "description"])
writer.writeheader()
for e in self.expenses:
writer.writerow(e.to_dict())
Fill in the pieces yourself rather than copying this directly. In particular, decide for yourself whether Expense should be a plain class or a @dataclass.
Bonus Extensions
- Add a
filter_by_category(category)method that returns only matching expenses, written as a generator (from the next module) once you get there. - Add a custom
InvalidExpenseErrorand raise it fromadd_expenseifamountis negative or zero. - Add a small command-line interface with
argparse(from Module 5) that lets you add an expense or print a summary without editing the script itself:python3 tracker.py add 42.50 groceries "weekly shop".
Try It
- Build
ExpenseandExpenseTracker, add a handful of expenses across at least two categories, and printtotal()andtotal_by_category(). - Save the tracker to a JSON file, then write a second small script that loads it back and confirms every expense is there.
- Export to CSV and open it in a spreadsheet program to confirm it looks right.
- Run your script twice in a row without clearing the data file, and confirm the second run picks up where the first left off.
- Attempt at least one bonus extension.
Recap
You’ve now built a program whose data genuinely outlives a single run, the real point of file I/O. Combining a class, JSON persistence, CSV export, and proper path handling in one project is a meaningfully bigger step than any single exercise in this module on its own.
Next module: generators, iterators, and decorators.