Capstone: Expense Tracker

Python Projects (Beginner)
Course 4 ยท Chapter 8 ยท Capstone: Expense Tracker

๐Ÿ’ฐ Capstone: Expense Tracker

The final project โ€” and the final chapter of the entire Python track. An expense tracker ties together everything this course has built: the load/save JSON pattern from Chapters 4-6, dict-based records, dict-based summarization (Course 1, Chapter 7's word-frequency idea, here counting money instead of words), and โ€” for the first time in this course โ€” try/except built directly into the app itself, rather than left as an extension.

๐ŸŽฎ What We're Building

An expense tracker: log expenses with an amount, category, and description; list them all; see totals by category and overall; delete a mistaken entry โ€” all persisted to JSON, and all resilient to bad input.

Step 1: Representing an Expense

expense = {"amount": 12.50, "category": "Food", "description": "Lunch"}

Step 2: Load & Save

import json
from pathlib import Path

FILE = Path("expenses.json")

def load_expenses():
    if not FILE.exists():
        return []
    return json.loads(FILE.read_text())

def save_expenses(expenses):
    FILE.write_text(json.dumps(expenses, indent=2))

By this point in the course, this pattern should feel completely familiar โ€” the fourth time this exact load_/save_ shape has appeared, after the to-do app, contact book, and scraper.

Step 3: Adding an Expense โ€” With Real Error Handling

def add_expense(expenses):
    try:
        amount = float(input("Amount: "))
    except ValueError:
        print("That's not a valid number โ€” expense not added.")
        return

    if amount <= 0:
        print("Amount must be positive โ€” expense not added.")
        return

    category = input("Category: ")
    description = input("Description: ")
    expenses.append({"amount": amount, "category": category, "description": description})
    print("Expense added.")

Chapters 1-7 flagged crash-prone float(input(...)) calls in warn-boxes and left the fix as an extension. Here, it's built in directly: try/except ValueError (Course 2, Chapter 3) catches non-numeric input cleanly, and a guard clause (Chapter 2's own technique) rejects a zero or negative amount as a separate, sensible business rule โ€” two different kinds of "bad input," handled with the two different tools each one actually calls for.

Step 4: Viewing and Summarizing

def list_expenses(expenses):
    if not expenses:
        print("No expenses recorded yet.")
        return
    for i, e in enumerate(expenses, start=1):
        print(f"{i}. ${e['amount']:.2f} โ€” {e['category']} โ€” {e['description']}")

def total_by_category(expenses):
    totals = {}
    for e in expenses:
        totals[e["category"]] = totals.get(e["category"], 0) + e["amount"]
    return totals

def total_spending(expenses):
    return sum(e["amount"] for e in expenses)

total_by_category reuses Course 1, Chapter 7's word-frequency-counter pattern exactly โ€” totals.get(category, 0) + amount in place of counts.get(word, 0) + 1, summing dollar amounts per category instead of counting word occurrences. total_spending reuses the generator-expression-plus-sum() pattern from Course 2, Chapter 5 and Course 3's own performance chapter.

Step 5: Deleting an Expense

def delete_expense(expenses, index):
    if 0 <= index < len(expenses):
        del expenses[index]
        print("Deleted.")
    else:
        print("Invalid expense number.")

Same bounds-checked del as Chapter 6's contact book โ€” and same rule for the menu loop below: always list_expenses() immediately before asking for a number to delete, exactly the fix Chapter 6's warn-box called for.

๐Ÿ The Complete Expense Tracker

import json
from pathlib import Path

FILE = Path("expenses.json")

def load_expenses():
    if not FILE.exists():
        return []
    return json.loads(FILE.read_text())

def save_expenses(expenses):
    FILE.write_text(json.dumps(expenses, indent=2))

def add_expense(expenses):
    try:
        amount = float(input("Amount: "))
    except ValueError:
        print("That's not a valid number โ€” expense not added.")
        return

    if amount <= 0:
        print("Amount must be positive โ€” expense not added.")
        return

    category = input("Category: ")
    description = input("Description: ")
    expenses.append({"amount": amount, "category": category, "description": description})
    print("Expense added.")

def list_expenses(expenses):
    if not expenses:
        print("No expenses recorded yet.")
        return
    for i, e in enumerate(expenses, start=1):
        print(f"{i}. ${e['amount']:.2f} โ€” {e['category']} โ€” {e['description']}")

def total_by_category(expenses):
    totals = {}
    for e in expenses:
        totals[e["category"]] = totals.get(e["category"], 0) + e["amount"]
    return totals

def total_spending(expenses):
    return sum(e["amount"] for e in expenses)

def delete_expense(expenses, index):
    if 0 <= index < len(expenses):
        del expenses[index]
        print("Deleted.")
    else:
        print("Invalid expense number.")

expenses = load_expenses()

while True:
    print("\n1. Add  2. List  3. Totals by Category  4. Total Spending  5. Delete  6. Quit")
    choice = input("Choose an option: ")

    if choice == "1":
        add_expense(expenses)
        save_expenses(expenses)
    elif choice == "2":
        list_expenses(expenses)
    elif choice == "3":
        for category, total in total_by_category(expenses).items():
            print(f"{category}: ${total:.2f}")
    elif choice == "4":
        print(f"Total spending: ${total_spending(expenses):.2f}")
    elif choice == "5":
        list_expenses(expenses)
        try:
            num = int(input("Expense number to delete: "))
        except ValueError:
            print("That's not a valid number.")
            continue
        delete_expense(expenses, num - 1)
        save_expenses(expenses)
    elif choice == "6":
        print("Goodbye!")
        break
    else:
        print("Not a valid option, try again.")

Option 5's own try/except ValueError around int(input(...)) reuses continue from Course 1, Chapter 4 โ€” skipping straight back to the top of the menu loop rather than letting a bad number crash the whole program, the same protective instinct add_expense applied to the amount field.

File I/O

load_expenses/save_expenses โ€” the same JSON pattern from every chapter since 4.

Functions

Each responsibility โ€” add, list, total, delete โ€” is its own small function.

Dict-based summarization

total_by_category reuses Course 1's word-frequency-counter pattern.

Error handling, built in

try/except around every risky input โ€” not left as an exercise this time.

๐ŸŽ“ Course Complete!

That's all 8 projects of Python Projects โ€” a guessing game, calculator, password generator, to-do app, quiz, contact book, web scraper, and this expense tracker capstone โ€” and with it, the complete Python track: Fundamentals โ†’ Intermediate โ†’ Advanced โ†’ Projects, 33 chapters across 4 courses. Every tool from every course ended up here, put to real, practical use.