Python Series track

Project 9 · Practice Projects

Project 9 — Budget Tracker

Track income and expenses by category. Generate monthly summaries, spending breakdowns, and savings rate reports — all stored in CSV.

Practice Projects1 min readFree lesson

Difficulty: Lv ★★★ | Prerequisite: Intermediate Week 5-6 (Exceptions, JSON I/O)

Goal

Income/Expenditure Household ledger that stores data permanently in a JSON file

Requirements

  • File: ledger.json (data preserved after exit)
  • Commands: add income|expense amount category memo, list, summary, del <number>
  • Totals and balances by category
  • Handle invalid input with exceptions

Challenge Task

  • Monthly summary (2026-05 filter)
  • Bar graph by category (#)
  • Single-line command (add expense 5000 food lunch)

Assignment

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

solution.pyPython
"""Household Ledger — JSON Permanent Storage"""
import json
from pathlib import Path
from datetime import date

DB = Path("ledger.json")

def load():
    if not DB.exists():
        return []
    try:
        return json.loads(DB.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        print("⚠️ ledger.json corrupted — starting with an empty ledger")
        return []

def save(records):
    DB.write_text(
        json.dumps(records, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )

def add(records):
    t = input("Income/Expense:").strip()
    if t not in ("Income", "Expense"):
        print("'Income' or 'Expense'"); return
    try:
        amount = int(input("Amount:"))
        if amount < 0:
            raise ValueError
    except ValueError:
        print("Enter a positive integer"); return
    category = input("Category:").strip()
    memo = input("Memo:").strip()
    records.append({
        "type": t, "amount": amount, "category": category,
        "memo": memo, "date": date.today().isoformat(),
    })
    save(records)
    print("Added")

def show(records):
    if not records:
        print("(Empty)"); return
    for i, r in enumerate(records, 1):
        sign = "+" if r["type"] == "Income" else "-"
        print(f"{i:>3}. {r['date']} {sign}{r['amount']:>10,} KRW [{r['category']:<6}] {r['memo']}")

def summary(records):
    income = sum(r["amount"] for r in records if r["type"] == "Income")
    expense = sum(r["amount"] for r in records if r["type"] == "Expense")
    print(f"Income: {income:>12,} KRW")
    print(f"Expense: {expense:>12,} KRW")
    print(f"Balance: {income - expense:>12,} KRW")

    by_cat = {}
    for r in records:
        if r["type"] == "Expense":
            by_cat[r["category"]] = by_cat.get(r["category"], 0) + r["amount"]
    if by_cat:
        max_amt = max(by_cat.values())
        print("\n[By Expense Category]")
        for cat, amt in sorted(by_cat.items(), key=lambda x: -x[1]):
            bar = "#" * int(40 * amt / max_amt)
            print(f"  {cat:<8} {amt:>10,}  {bar}")

def main():
    records = load()
    print(f"Loaded: {len(records)} items")
    while True:
        cmd = input("\nCommand (add/list/summary/del <n>/quit):").strip()
        if not cmd: continue
        if cmd == "quit": break
        elif cmd == "add": add(records)
        elif cmd == "list": show(records)
        elif cmd == "summary": summary(records)
        elif cmd.startswith("del "):
            try:
                idx = int(cmd.split()[1]) - 1
                if 0 <= idx < len(records):
                    del records[idx]; save(records); print("Delete")
            except (ValueError, IndexError):
                print("Enter a valid number")

if __name__ == "__main__":
    main()

Learn this lesson with a mentor

Work through the Python Series track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now