Project · after Level 3

Expense Tracker & Report 💰

Log expenses with a category and an amount, store them in a CSV a spreadsheet could open, and generate a report totalled by category. It is small-business software in miniature, and it uses the CSV and grouping skills from Level 3 in anger.

Difficulty 🐍🐍🐍🐍🐍

📋 Build this

  • Append each expense (date, category, amount, note) as a row in a CSV file.
  • Read the CSV back with the csv module, converting amounts to numbers.
  • Produce a report: total per category, sorted by spend, and a grand total.
  • Handle the file not existing yet.

Hints, if you want them

Try the spec cold first. Open a hint only when you are properly stuck; the struggle is where the learning is.

Hint 1: Writing CSV properly
Use csv.writer with newline="", never string-join on commas (Lesson 23). A note with a comma in it will break a naive writer.
Hint 2: Reading and totalling
csv.DictReader gives each row as a dict. Remember every value is a string, so convert the amount with float() (Lesson 23).
Hint 3: Grouping
Total per category with collections.defaultdict(float) or a plain dict and .get, then sort the items by total, descending (Lesson 15).

The reference solution

Yours does not need to match this. There are many good ways to build any of these. Compare only after you have your own working.

Reveal the reference solution
import csv
from collections import defaultdict
from pathlib import Path

FILE = Path("expenses.csv")


def add_expense(date, category, amount, note):
    """Append one expense as a CSV row, creating the header if new."""
    new_file = not FILE.exists()
    with open(FILE, "a", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        if new_file:
            writer.writerow(["date", "category", "amount", "note"])
        writer.writerow([date, category, f"{amount:.2f}", note])


def report():
    """Total per category, sorted by spend, plus a grand total."""
    totals = defaultdict(float)
    with open(FILE, newline="", encoding="utf-8") as f:
        for row in csv.DictReader(f):
            totals[row["category"]] += float(row["amount"])

    for category, total in sorted(totals.items(), key=lambda kv: kv[1], reverse=True):
        print(f"{category:12} {total:8.2f}")
    print(f"{'TOTAL':12} {sum(totals.values()):8.2f}")


# a scripted run so the output is reproducible
add_expense("2026-08-01", "food", 12.50, "lunch, with a comma")
add_expense("2026-08-02", "transport", 4.20, "bus")
add_expense("2026-08-03", "food", 30.00, "dinner")
report()
food            42.50
transport        4.20
TOTAL           46.70

Stretch goals

+250 XP