CodingNic

File I/O

Mini Project: Personal Expense Tracker

File I/O 50 min read

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 pathlib in 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) with amount, category, and description
  • A to_dict() method returning a plain dictionary: the shape you’ll actually save to JSON

ExpenseTracker

  • add_expense(expense): appends a new Expense to the tracker’s collection
  • total(): returns the sum of every expense’s amount
  • total_by_category(): returns a dictionary mapping each category to its total spent
  • save_to_file(filename): writes every expense to a JSON file using json.dump(..., indent=2)
  • load_from_file(filename): reads a JSON file back into a list of Expense objects; if the file doesn’t exist yet, start with an empty tracker instead of raising an error
  • export_to_csv(filename): writes every expense out with amount, category, and description columns using csv.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

python
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 InvalidExpenseError and raise it from add_expense if amount is 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

  1. Build Expense and ExpenseTracker, add a handful of expenses across at least two categories, and print total() and total_by_category().
  2. Save the tracker to a JSON file, then write a second small script that loads it back and confirms every expense is there.
  3. Export to CSV and open it in a spreadsheet program to confirm it looks right.
  4. Run your script twice in a row without clearing the data file, and confirm the second run picks up where the first left off.
  5. 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.