Capstone: Expense Tracker
๐ฐ Capstone: Expense Tracker
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.