Project 2

File-Backed Expense & Task Tracker

~1 hour

1. Project Overview & Architecture

Welcome to Major Project 2: File-Backed Expense & Task Tracker! In this project, you will elevate your Python applications from temporary in-memory programs to persistent software that saves and loads real data from the file system. You will apply key concepts from Modules 4–7: structured data (lists of dictionaries), JSON file handling, string manipulation, and comprehensive try-except exception management.

Project Core Objectives

  • Persistent Storage: Load existing data on startup and automatically save changes to a local .json file on disk.
  • CRUD Operations: Implement full Create, Read, and Summary operations for financial transactions.
  • Robust Error Recovery: Catch file corruption, missing files, and invalid numeric conversions without crashing the application.
  • Data Aggregation: Calculate total spending, category breakdowns, and average transaction costs using Python loops and list comprehension techniques.

2. Functional Requirements & Feature Specifications

Your Expense Tracker must implement the following mandatory components:

Feature Component Functional Specification Python Mechanism
File I/O Engine Reads/writes standard JSON data files safely. json.dump(), json.load(), pathlib.Path
Numeric Sanitization Guarantees expenses are valid positive floating-point numbers. try-except ValueError inside input loops.
Category Summary Group expenses by category (Food, Utilities, Entertainment) with totals. Dictionary aggregation (dict.get() logic).
Corrupted File Protection Recovers cleanly if the local JSON file is unreadable or malformed. json.JSONDecodeError catch blocks.

3. Complete Reference Implementation

Review the complete Python implementation below to see how persistent JSON storage and input error handling work together:

import json
from pathlib import Path
from datetime import datetime

# Persistent storage file path
DATA_FILE = Path("expenses.json")

def load_expenses():
    """Loads expenses from JSON file. Returns an empty list if file doesn't exist or is corrupted."""
    if not DATA_FILE.exists():
        return []
    
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except (json.JSONDecodeError, IOError) as err:
        print(f"⚠️ Warning: Could not read '{DATA_FILE}' ({err}). Starting with fresh data.")
        return []

def save_expenses(expenses):
    """Saves current expense list to JSON storage with clean formatting."""
    try:
        with open(DATA_FILE, "w", encoding="utf-8") as file:
            json.dump(expenses, file, indent=4)
        print("💾 Data saved successfully.")
    except IOError as err:
        print(f"❌ Critical Error: Unable to save data ({err}).")

def get_positive_float(prompt):
    """Prompts until a valid positive float is provided."""
    while True:
        try:
            val = float(input(prompt).strip())
            if val > 0:
                return val
            print("⚠️ Amount must be greater than $0.00.")
        except ValueError:
            print("⚠️ Invalid number format! Please enter a numeric value (e.g., 12.50).")

def add_expense(expenses):
    """Collects user details and appends a new expense record."""
    print("\n--- ➕ Add New Expense ---")
    title = input("Enter description/title: ").strip()
    while not title:
        print("⚠️ Title cannot be empty.")
        title = input("Enter description/title: ").strip()

    category = input("Enter category (e.g., Food, Transport, Utilities): ").strip().title()
    if not category:
        category = "General"

    amount = get_positive_float("Enter amount ($): ")
    date_str = datetime.now().strftime("%Y-%m-%d %H:%M")

    record = {
        "title": title,
        "category": category,
        "amount": round(amount, 2),
        "date": date_str
    }

    expenses.append(record)
    save_expenses(expenses)
    print(f"✅ Added: '${title}' (${amount:.2f}) under [{category}]")

def view_expenses(expenses):
    """Displays all recorded expenses in a clean formatted table."""
    print("\n--- 📝 All Recorded Expenses ---")
    if not expenses:
        print("No expenses recorded yet!")
        return

    print(f"{'#':<4} {'Date':<17} {'Category':<15} {'Title':<20} {'Amount':>10}")
    print("-" * 70)
    for idx, item in enumerate(expenses, start=1):
        print(f"{idx:<4} {item['date']:<17} {item['category']:<15} {item['title']:<20} ${item['amount']:>9.2f}")
    print("-" * 70)

def view_summary(expenses):
    """Calculates and displays financial summaries and category totals."""
    print("\n--- 📊 Expense Summary & Analytics ---")
    if not expenses:
        print("No data available for summary calculation.")
        return

    total_spent = sum(item["amount"] for item in expenses)
    avg_spent = total_spent / len(expenses)

    # Aggregate by category
    by_category = {}
    for item in expenses:
        cat = item["category"]
        by_category[cat] = by_category.get(cat, 0.0) + item["amount"]

    print(f"Total Transactions: {len(expenses)}")
    print(f"Total Expenditure:  ${total_spent:,.2f}")
    print(f"Average Expense:    ${avg_spent:,.2f}\n")
    print("Spending by Category:")
    for cat, cat_total in by_category.items():
        percentage = (cat_total / total_spent) * 100
        print(f"  • {cat:<15}: ${cat_total:>8.2f} ({percentage:>5.1f}%)")

def main():
    expenses = load_expenses()

    while True:
        print("\n==================================")
        print("    💰 FILE-BACKED EXPENSE TRACKER")
        print("==================================")
        print("1) Add New Expense")
        print("2) View All Expenses")
        print("3) View Summary & Category Stats")
        print("4) Exit Program")

        choice = input("\nSelect choice (1-4): ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            view_expenses(expenses)
        elif choice == "3":
            view_summary(expenses)
        elif choice == "4":
            print("\nExiting Expense Tracker. Goodbye!")
            break
        else:
            print("⚠️ Invalid choice. Please select 1, 2, 3, or 4.")

if __name__ == "__main__":
    main()

4. Common Project Pitfalls

⚠️ Project Watchlist:

  • Forgetting File Encoding: Always specify encoding="utf-8" when opening files to avoid platform-dependent character encoding issues between Windows and Mac/Linux.
  • Unsafe Float Conversion: Direct call to float(input()) without a try-except block will crash the entire program if a user types currency symbols like "$15".
  • In-Memory Memory Disconnect: Modifying the expenses list in memory without calling save_expenses() causes data loss if the terminal exits abruptly.

5. Interactive Project Workspace

Project Workspace

Build & Test: Expense Tracker

Use the code editor below to customize your expense tracker. Try adding a feature to filter expenses by category or export a text receipt!

0% complete
Project Feature Checklist

6. Key Takeaways & Milestone Achievement

  • JSON provides a human-readable, highly compatible structure for storing dictionary and list data permanently on disk.
  • Wrap file reads inside try-except blocks to safeguard against missing or corrupted storage files.
  • Input sanitization prevents string conversion crashes before bad data reaches your state storage.

🏆 Milestone 2 Complete

Congratulations on completing Major Project 2! Up next: **Major Project 3: Desktop Automated File & System Organizer** where you will write automation scripts to interact with your operating system!