Complete Project 3: Expense Tracker

An Expense Tracker is like a digital accountant in your pocket. It helps you track where your money goes, just like keeping receipts but with automatic categorization and insightful reports.
The Expense Tracker is a comprehensive personal finance application that demonstrates real‑world data management, analysis, and reporting. It allows users to record expenses, categorise them, set monthly budgets, view statistics, generate reports, and export data. This project brings together file handling (JSON/CSV), data structures (lists, dictionaries, defaultdict), datetime manipulation, aggregation, and a menu‑driven console interface.

**Core Features:**
- **Add Expense** – Record a transaction with description, amount, category (10 predefined categories), date, and payment method (cash/card/online).
- **View All Expenses** – Chronological list (newest first) with total.
- **View by Category** – Grouped totals with percentage bar chart and top 3 expenses per category.
- **View by Month** – Monthly summaries with top spending categories.
- **Budget Management** – Set monthly budgets per category, view budget vs. actual, receive alerts when exceeding or nearing budget (80% warning, 100% alert).
- **Reports** – Monthly summary, category analysis, spending trends (last 6 months), top expenses, payment method analysis.
- **Search** – Keyword search in description or category.
- **Export** – Save all expenses to CSV for external analysis (spreadsheets).
- **Delete** – Remove an expense with confirmation and automatic ID renumbering.
- **Data Persistence** – All expenses stored in `expenses.json`; budgets stored in `budget.json`.

**How It Works:**

The `ExpenseTracker` class initialises by loading existing expenses from `expenses.json` and budgets from `budget.json`. The `categories` list defines the 10 default categories. Each expense is a dictionary with keys: `id`, `description`, `amount`, `category`, `date`, `payment_method`, and `added_date`.

**Adding an Expense (`add_expense`):**
- Prompts for description (required), amount (positive number validation), category (menu selection), date (YYYY‑MM‑DD validation, defaults to today), payment method.
- Creates a new expense dictionary, appends to `self.expenses`, saves to JSON, and calls `check_budget_alert()` to see if the new expense pushes the category over budget.

**Viewing Expenses:**
- `view_all_expenses` sorts by date (newest first) and prints a formatted table.
- `view_by_category` groups expenses using `defaultdict`, calculates totals and percentages, displays a simple bar chart (`█` characters), and shows the top 3 expenses in each category.
- `view_by_month` groups by year‑month, formats month names, and shows top categories for that month.

**Budget Management:**
- `manage_budget` allows setting a monthly limit per category (only categories with a budget >0 appear).
- `show_budget_vs_actual` calculates spending for the current month (from the 1st to today) and compares with budget, showing remaining amount and percentage used. It also highlights totals and warns if over budget or above 80%.
- Budget alerts are triggered when adding an expense: a warning at ≥80%, an alert at >100%.

**Reports (`generate_reports`):**
- **Monthly Summary** – Total, number of expenses, average per day, projected monthly total, comparison with previous month (with ▲/▼ symbol).
- **Category Analysis** – Detailed per‑category spending over all time.
- **Spending Trends** – (Placeholder method; can be extended with monthly totals over last 6 months).
- **Top Expenses** – Highest single expenses.
- **Payment Method Analysis** – Breakdown by cash/card/online.

**Search and Delete:**
- `search_expenses` performs a case‑insensitive search in description and category, then calls `view_all_expenses` with the filtered list.
- `delete_expense` shows all expenses with IDs, asks for ID, confirms deletion, removes from list, then reassigns IDs sequentially and saves.

**Data Export:**
- `export_data` writes all expenses to a CSV file with a timestamped filename, using the `csv.DictWriter` module.

**Error Handling:**
- Input validation for amount (float, >0), category (1‑10), date (valid format).
- File I/O errors are caught when loading/saving – the app starts fresh with a warning.
- The main loop catches `KeyboardInterrupt` and general exceptions to prevent crashes.

**Why This Project Is Important:**
- **Real‑world utility** – You can actually use it to track personal finances.
- **Data persistence** – JSON files ensure data survives program restarts.
- **Aggregation & Reporting** – Demonstrates grouping, summing, averaging, and formatting output.
- **Budget alerts** – Introduces proactive monitoring and user notifications.
- **Export capability** – Shows how to interoperate with external tools (spreadsheets).
- **Extensible design** – You can easily add charts, database storage, or a GUI.

**How to Run:**
Save the code as `expense_tracker.py` and run `python expense_tracker.py`. The first run creates `expenses.json` and `budget.json`. All data is saved automatically after each change.

**Potential Enhancements (for practice):**
- Add a recurring expense feature (monthly subscriptions).
- Implement a graphical interface using `tkinter` or a web dashboard with `Flask`.
- Add data visualisation (pie charts) using `matplotlib`.
- Include a savings goal tracker.
- Allow editing existing expenses.
- Import expenses from bank CSV files.

**Real‑World Skills:**
This project mimics the architecture of many financial apps: data entry, validation, storage, aggregation, reporting, and export. It is an excellent portfolio piece demonstrating proficiency in Python data handling, file I/O, date manipulation, and user interaction.
# EXPENSE TRACKER APPLICATION
import json
import csv
import os
from datetime import datetime, timedelta
from collections import defaultdict

print("EXPENSE TRACKER")
print("=" * 50)

class ExpenseTracker:
    """A comprehensive expense tracking application"""
    
    def __init__(self, data_file="expenses.json"):
        self.data_file = data_file
        self.expenses = self.load_expenses()
        self.categories = [
            "Food", "Transportation", "Housing", "Utilities", 
            "Entertainment", "Healthcare", "Shopping", "Education", 
            "Travel", "Other"
        ]
        self.budget = self.load_budget()
    
    def load_expenses(self):
        """Load expenses from JSON file"""
        if os.path.exists(self.data_file):
            try:
                with open(self.data_file, 'r') as file:
                    return json.load(file)
            except (json.JSONDecodeError, IOError):
                print("Warning: Could not load expenses. Starting fresh.")
                return []
        return []
    
    def save_expenses(self):
        """Save expenses to JSON file"""
        try:
            with open(self.data_file, 'w') as file:
                json.dump(self.expenses, file, indent=2)
        except IOError:
            print("Error: Could not save expenses.")
    
    def load_budget(self):
        """Load budget from file"""
        budget_file = "budget.json"
        if os.path.exists(budget_file):
            try:
                with open(budget_file, 'r') as file:
                    return json.load(file)
            except (json.JSONDecodeError, IOError):
                return {category: 0 for category in self.categories}
        return {category: 0 for category in self.categories}
    
    def save_budget(self):
        """Save budget to file"""
        try:
            with open("budget.json", 'w') as file:
                json.dump(self.budget, file, indent=2)
        except IOError:
            print("Error: Could not save budget.")
    
    def display_menu(self):
        """Display main menu"""
        print("\n" + "💵" * 20)
        print("EXPENSE TRACKER MENU")
        print("💵" * 20)
        print("1. Add New Expense")
        print("2. View All Expenses")
        print("3. View Expenses by Category")
        print("4. View Expenses by Month")
        print("5. Set/View Budget")
        print("6. Generate Reports")
        print("7. Search Expenses")
        print("8. Export Data")
        print("9. Delete Expense")
        print("10. Exit")
        print("-" * 40)
        
        # Show quick summary
        if self.expenses:
            today = datetime.now().date()
            month_start = today.replace(day=1)
            month_expenses = [e for e in self.expenses 
                             if datetime.strptime(e["date"], "%Y-%m-%d").date() >= month_start]
            
            if month_expenses:
                total_month = sum(e["amount"] for e in month_expenses)
                print(f"This month's spending: ${total_month:.2f}")
    
    def add_expense(self):
        """Add a new expense"""
        print("\n" + "-" * 40)
        print("ADD NEW EXPENSE")
        print("-" * 40)
        
        # Get expense details
        description = input("Description: ").strip()
        if not description:
            print("Description cannot be empty!")
            return
        
        amount = self.get_amount()
        category = self.get_category()
        date = self.get_date()
        payment_method = input("Payment method (cash/card/online): ").strip().lower() or "card"
        
        # Create expense record
        expense = {
            "id": len(self.expenses) + 1,
            "description": description,
            "amount": amount,
            "category": category,
            "date": date,
            "payment_method": payment_method,
            "added_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        }
        
        self.expenses.append(expense)
        self.save_expenses()
        
        print(f"\n✅ Expense added successfully!")
        print(f"   {description}: ${amount:.2f} under {category}")
        
        # Check if over budget for this category
        self.check_budget_alert(category, amount)
    
    def get_amount(self):
        """Get amount with validation"""
        while True:
            try:
                amount = float(input("Amount: $").strip())
                if amount <= 0:
                    print("Amount must be greater than 0")
                    continue
                return amount
            except ValueError:
                print("Please enter a valid number")
    
    def get_category(self):
        """Get category with validation"""
        print("\nCategories:")
        for i, category in enumerate(self.categories, 1):
            print(f"  {i}. {category}")
        
        while True:
            try:
                choice = input("\nSelect category (1-{}): ".format(len(self.categories))).strip()
                if not choice:
                    return "Other"
                
                idx = int(choice) - 1
                if 0 <= idx < len(self.categories):
                    return self.categories[idx]
                else:
                    print(f"Please enter 1-{len(self.categories)}")
            except ValueError:
                print("Please enter a valid number")
    
    def get_date(self):
        """Get date with validation"""
        while True:
            date_str = input("Date (YYYY-MM-DD, Enter for today): ").strip()
            if not date_str:
                return datetime.now().strftime("%Y-%m-%d")
            
            try:
                datetime.strptime(date_str, "%Y-%m-%d")
                return date_str
            except ValueError:
                print("Invalid date format. Please use YYYY-MM-DD")
    
    def view_all_expenses(self, expenses=None):
        """View all expenses"""
        if expenses is None:
            expenses = self.expenses
        
        if not expenses:
            print("\nNo expenses found.")
            return
        
        # Sort by date (newest first)
        sorted_expenses = sorted(expenses, 
                                key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"), 
                                reverse=True)
        
        print("\n" + "=" * 80)
        print(f"ALL EXPENSES ({len(sorted_expenses)} total)")
        print("=" * 80)
        print(f"{'Date':12} {'Category':15} {'Description':25} {'Amount':10} {'Payment':10}")
        print("-" * 80)
        
        total = 0
        for expense in sorted_expenses:
            date = expense["date"]
            category = expense["category"]
            description = expense["description"][:23] + "..." if len(expense["description"]) > 23 else expense["description"]
            amount = expense["amount"]
            payment = expense["payment_method"]
            
            print(f"{date:12} {category:15} {description:25} ${amount:8.2f} {payment:10}")
            total += amount
        
        print("-" * 80)
        print(f"{'TOTAL':54} ${total:8.2f}")
        print("=" * 80)
    
    def view_by_category(self):
        """View expenses grouped by category"""
        if not self.expenses:
            print("\nNo expenses found.")
            return
        
        # Group by category
        category_totals = defaultdict(float)
        category_expenses = defaultdict(list)
        
        for expense in self.expenses:
            category = expense["category"]
            category_totals[category] += expense["amount"]
            category_expenses[category].append(expense)
        
        print("\n" + "=" * 60)
        print("EXPENSES BY CATEGORY")
        print("=" * 60)
        
        grand_total = 0
        for category in sorted(category_totals.keys()):
            total = category_totals[category]
            count = len(category_expenses[category])
            percentage = (total / sum(category_totals.values()) * 100) if sum(category_totals.values()) > 0 else 0
            
            print(f"\n📁 {category.upper()}:")
            print(f"   Total: ${total:.2f} ({percentage:.1f}%)")
            print(f"   Count: {count} expense(s)")
            
            # Show bar chart
            max_bar_length = 30
            bar_length = int((percentage / 100) * max_bar_length)
            bar = "█" * bar_length
            print(f"   {bar} {percentage:.1f}%")
            
            # Show top 3 expenses in this category
            sorted_category = sorted(category_expenses[category], 
                                   key=lambda x: x["amount"], 
                                   reverse=True)[:3]
            for exp in sorted_category:
                print(f"      • {exp['description'][:20]}: ${exp['amount']:.2f} ({exp['date']})")
            
            grand_total += total
        
        print("\n" + "=" * 60)
        print(f"GRAND TOTAL: ${grand_total:.2f}")
        print("=" * 60)
    
    def view_by_month(self):
        """View expenses grouped by month"""
        if not self.expenses:
            print("\nNo expenses found.")
            return
        
        # Group by month
        month_totals = defaultdict(float)
        month_expenses = defaultdict(list)
        
        for expense in self.expenses:
            date_obj = datetime.strptime(expense["date"], "%Y-%m-%d")
            month_key = date_obj.strftime("%Y-%m")
            month_totals[month_key] += expense["amount"]
            month_expenses[month_key].append(expense)
        
        print("\n" + "=" * 60)
        print("EXPENSES BY MONTH")
        print("=" * 60)
        
        for month in sorted(month_totals.keys(), reverse=True):
            total = month_totals[month]
            count = len(month_expenses[month])
            
            # Format month name
            month_obj = datetime.strptime(month + "-01", "%Y-%m-%d")
            month_name = month_obj.strftime("%B %Y")
            
            print(f"\n📅 {month_name}:")
            print(f"   Total: ${total:.2f}")
            print(f"   Count: {count} expense(s)")
            
            # Show categories for this month
            month_cats = defaultdict(float)
            for exp in month_expenses[month]:
                month_cats[exp["category"]] += exp["amount"]
            
            # Show top 3 categories
            sorted_cats = sorted(month_cats.items(), key=lambda x: x[1], reverse=True)[:3]
            for cat, amount in sorted_cats:
                percentage = (amount / total * 100) if total > 0 else 0
                print(f"      • {cat}: ${amount:.2f} ({percentage:.1f}%)")
        
        print("\n" + "=" * 60)
    
    def manage_budget(self):
        """Set and view budget"""
        print("\n" + "=" * 50)
        print("BUDGET MANAGEMENT")
        print("=" * 50)
        
        print("\nCurrent Budget:")
        print("-" * 40)
        total_budget = 0
        for category in self.categories:
            budget_amount = self.budget.get(category, 0)
            if budget_amount > 0:
                print(f"{category:15}: ${budget_amount:.2f}")
                total_budget += budget_amount
        
        if total_budget == 0:
            print("No budget set yet.")
        else:
            print(f"\nTotal Monthly Budget: ${total_budget:.2f}")
        
        print("\nOptions:")
        print("1. Set budget for a category")
        print("2. View budget vs actual")
        print("3. Clear all budgets")
        print("4. Back to main menu")
        
        choice = input("\nEnter choice: ").strip()
        
        if choice == "1":
            print("\nSelect category to set budget:")
            for i, category in enumerate(self.categories, 1):
                current = self.budget.get(category, 0)
                print(f"  {i}. {category}: ${current:.2f}")
            
            try:
                cat_idx = int(input("\nCategory number: ")) - 1
                if 0 <= cat_idx < len(self.categories):
                    category = self.categories[cat_idx]
                    amount = float(input(f"Monthly budget for {category}: $"))
                    self.budget[category] = amount
                    self.save_budget()
                    print(f"✅ Budget for {category} set to ${amount:.2f}")
                else:
                    print("Invalid category number.")
            except ValueError:
                print("Please enter a valid number.")
        
        elif choice == "2":
            self.show_budget_vs_actual()
        
        elif choice == "3":
            confirm = input("Are you sure you want to clear all budgets? (y/n): ")
            if confirm.lower() == 'y':
                self.budget = {category: 0 for category in self.categories}
                self.save_budget()
                print("✅ All budgets cleared.")
        
        elif choice == "4":
            return
        
        else:
            print("Invalid choice.")
    
    def check_budget_alert(self, category, amount):
        """Check if expense exceeds budget"""
        budget_amount = self.budget.get(category, 0)
        if budget_amount == 0:
            return
        
        # Calculate monthly total for this category
        today = datetime.now().date()
        month_start = today.replace(day=1)
        
        monthly_total = sum(
            e["amount"] for e in self.expenses 
            if e["category"] == category and 
            datetime.strptime(e["date"], "%Y-%m-%d").date() >= month_start
        )
        
        if monthly_total > budget_amount:
            print(f"\n⚠  BUDGET ALERT: You've exceeded your {category} budget!")
            print(f"   Budget: ${budget_amount:.2f}, Spent: ${monthly_total:.2f}")
            print(f"   Over budget by: ${monthly_total - budget_amount:.2f}")
        elif monthly_total >= budget_amount * 0.8:  # 80% of budget
            print(f"\n⚠  BUDGET WARNING: You've used {monthly_total/budget_amount*100:.0f}% of your {category} budget")
    
    def show_budget_vs_actual(self):
        """Show budget vs actual spending"""
        if not any(self.budget.values()):
            print("\nNo budget set yet.")
            return
        
        today = datetime.now().date()
        month_start = today.replace(day=1)
        
        print("\n" + "=" * 60)
        print("BUDGET VS ACTUAL (THIS MONTH)")
        print("=" * 60)
        
        print(f"\n{'Category':15} {'Budget':10} {'Spent':10} {'Remaining':12} {'% Used':8}")
        print("-" * 60)
        
        total_budget = 0
        total_spent = 0
        
        for category in sorted(self.budget.keys()):
            budget = self.budget.get(category, 0)
            if budget == 0:
                continue
            
            # Calculate monthly spending for this category
            monthly_spent = sum(
                e["amount"] for e in self.expenses 
                if e["category"] == category and 
                datetime.strptime(e["date"], "%Y-%m-%d").date() >= month_start
            )
            
            remaining = budget - monthly_spent
            percent_used = (monthly_spent / budget * 100) if budget > 0 else 0
            
            status = "✅" if percent_used <= 100 else "⚠"
            
            print(f"{category:15} ${budget:8.2f} ${monthly_spent:8.2f} ${remaining:10.2f} {percent_used:7.1f}% {status}")
            
            total_budget += budget
            total_spent += monthly_spent
        
        print("-" * 60)
        total_remaining = total_budget - total_spent
        total_percent = (total_spent / total_budget * 100) if total_budget > 0 else 0
        
        print(f"{'TOTAL':15} ${total_budget:8.2f} ${total_spent:8.2f} ${total_remaining:10.2f} {total_percent:7.1f}%")
        print("=" * 60)
        
        if total_percent > 100:
            print("\n⚠  You have exceeded your total monthly budget!")
        elif total_percent > 80:
            print("\n⚠  You have used more than 80% of your monthly budget")
    
    def generate_reports(self):
        """Generate various reports"""
        if not self.expenses:
            print("\nNo expenses to generate reports.")
            return
        
        print("\n" + "=" * 50)
        print("REPORTS")
        print("=" * 50)
        
        print("\nAvailable Reports:")
        print("1. Monthly Summary Report")
        print("2. Category Analysis Report")
        print("3. Spending Trends (Last 6 Months)")
        print("4. Top Expenses Report")
        print("5. Payment Method Analysis")
        
        try:
            choice = input("\nSelect report (1-5): ").strip()
            
            if choice == "1":
                self.monthly_summary_report()
            elif choice == "2":
                self.category_analysis_report()
            elif choice == "3":
                self.spending_trends_report()
            elif choice == "4":
                self.top_expenses_report()
            elif choice == "5":
                self.payment_method_report()
            else:
                print("Invalid choice.")
        except Exception as e:
            print(f"Error generating report: {e}")
    
    def monthly_summary_report(self):
        """Generate monthly summary report"""
        today = datetime.now().date()
        current_month = today.strftime("%Y-%m")
        
        # Get current month expenses
        month_expenses = [e for e in self.expenses 
                         if e["date"].startswith(current_month)]
        
        if not month_expenses:
            print(f"\nNo expenses for {current_month}")
            return
        
        total = sum(e["amount"] for e in month_expenses)
        avg_per_day = total / today.day if today.day > 0 else total
        
        print(f"\n📊 MONTHLY SUMMARY REPORT - {current_month}")
        print("=" * 60)
        print(f"Total Expenses: ${total:.2f}")
        print(f"Number of Expenses: {len(month_expenses)}")
        print(f"Average per Day: ${avg_per_day:.2f}")
        
        # Projected monthly total
        days_in_month = 30  # Simplified
        projected_total = (total / today.day * days_in_month) if today.day > 0 else total
        print(f"Projected Monthly Total: ${projected_total:.2f}")
        
        # Compare with previous month
        prev_month_date = today.replace(day=1) - timedelta(days=1)
        prev_month = prev_month_date.strftime("%Y-%m")
        prev_month_expenses = [e for e in self.expenses 
                              if e["date"].startswith(prev_month)]
        
        if prev_month_expenses:
            prev_total = sum(e["amount"] for e in prev_month_expenses)
            change = total - prev_total
            change_percent = (change / prev_total * 100) if prev_total > 0 else 0
            
            change_symbol = "▲" if change > 0 else "▼"
            print(f"\nComparison with {prev_month}:")
            print(f"  Previous Month: ${prev_total:.2f}")
            print(f"  Change: {change_symbol}${abs(change):.2f} ({change_percent:+.1f}%)")
        
        print("=" * 60)
    
    def search_expenses(self):
        """Search expenses by keyword"""
        if not self.expenses:
            print("\nNo expenses to search.")
            return
        
        keyword = input("\nSearch keyword (description/category): ").lower().strip()
        if not keyword:
            print("Please enter a keyword.")
            return
        
        results = []
        for expense in self.expenses:
            if (keyword in expense["description"].lower() or 
                keyword in expense["category"].lower()):
                results.append(expense)
        
        if results:
            print(f"\nFound {len(results)} expense(s) matching '{keyword}':")
            self.view_all_expenses(results)
        else:
            print(f"\nNo expenses found matching '{keyword}'.")
    
    def export_data(self):
        """Export data to CSV"""
        if not self.expenses:
            print("\nNo expenses to export.")
            return
        
        filename = f"expenses_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
        
        with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
            fieldnames = ['id', 'date', 'description', 'amount', 'category', 'payment_method', 'added_date']
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            
            writer.writeheader()
            for expense in self.expenses:
                writer.writerow(expense)
        
        print(f"\n✅ Expenses exported to {filename}")
        print(f"Total records: {len(self.expenses)}")
    
    def delete_expense(self):
        """Delete an expense"""
        if not self.expenses:
            print("\nNo expenses to delete.")
            return
        
        self.view_all_expenses()
        
        try:
            expense_id = int(input("\nEnter expense ID to delete: "))
            for i, expense in enumerate(self.expenses):
                if expense["id"] == expense_id:
                    confirm = input(f"Delete '{expense['description']}' (${expense['amount']})? (y/n): ")
                    if confirm.lower() == 'y':
                        deleted = self.expenses.pop(i)
                        self.save_expenses()
                        print(f"\n✅ Expense deleted: {deleted['description']}")
                        # Reassign IDs
                        for j, exp in enumerate(self.expenses, 1):
                            exp["id"] = j
                        self.save_expenses()
                    return
            print("\n❌ Expense not found.")
        except ValueError:
            print("\n❌ Please enter a valid number.")
    
    def run(self):
        """Run the expense tracker"""
        print("\n💰 Welcome to Expense Tracker!")
        print("Track your expenses and manage your budget.")
        
        while True:
            self.display_menu()
            
            try:
                choice = input("\nEnter your choice (1-10): ").strip()
                
                if choice == "1":
                    self.add_expense()
                elif choice == "2":
                    self.view_all_expenses()
                elif choice == "3":
                    self.view_by_category()
                elif choice == "4":
                    self.view_by_month()
                elif choice == "5":
                    self.manage_budget()
                elif choice == "6":
                    self.generate_reports()
                elif choice == "7":
                    self.search_expenses()
                elif choice == "8":
                    self.export_data()
                elif choice == "9":
                    self.delete_expense()
                elif choice == "10":
                    print("\n👋 Thank you for using Expense Tracker!")
                    print("Your financial data has been saved.")
                    break
                else:
                    print("\n❌ Invalid choice. Please enter 1-10.")
            except KeyboardInterrupt:
                print("\n\n👋 Goodbye!")
                break
            except Exception as e:
                print(f"\n❌ An error occurred: {e}")

# Run the application
if __name__ == "__main__":
    tracker = ExpenseTracker()
    tracker.run()

→ Run this code interactively