Build a Rupee Expense Tracker in Python

Build a CLI expense tracker in Python with JSON storage and category summaries.

Introduction

30 Second Summary

Everyone tracks where their money goes. Whether it's a quick chai or a monthly phone bill, knowing your spending patterns is the first step to managing your finances better.

In this project, you will build a command-line expense tracker in Python that records spending in Rupees, saves data between sessions, and shows you exactly where your money goes by category.

What You'll Build

A terminal-based expense tracker application that runs in VS Code's integrated terminal, with a menu system for adding, viewing, and analyzing expenses.

By the end of this project, you'll have:

  • A working Python CLI application with an interactive menu system for recording expenses in Rupees.
  • Persistent data storage using JSON so your expenses survive between sessions.
  • Category-based spending summaries that calculate totals and percentages to show where your money goes.
  • Secret Mission: Add monthly budget limits per category with overspending warnings.

Are there any prerequisites?

No prior Python experience is required. This project includes setup verification for all tools you need. As long as you have VS Code and Python installed on your computer, you are ready to start.

Before We Start

Every application starts with a solid foundation. Before writing any logic for your expense tracker, you need to confirm your tools are working and create a clean workspace.

In this step, you will verify that Python runs correctly on your machine, set up a dedicated project folder, and test your setup with a simple print statement.

In this step, get ready to:
  • Verify Python is installed and responding to commands.
  • Create the expense_tracker project folder and file.
  • Run a test print to confirm everything works.
Verify Python is working

Your expense tracker is a Python program, so the first thing to confirm is that Python responds to commands in your terminal.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open the integrated terminal by clicking Terminal in the menu bar, then clicking New Terminal.
  • Check your Python version by running this command:
python --version

You should see output like Python 3.x.x (the exact number does not matter as long as it starts with 3).

✔️ I see a version number

You're all set. Python is working correctly on your machine.

ⓧ Command not found

On Windows, Python sometimes uses a different command alias. Try running this instead:

py --version

If this shows a version number starting with 3, use py in place of python for all commands throughout this project.

Still not working?

Python may not be added to your system PATH. Reinstall Python from python.org and make sure to check the box that says Add Python to PATH during installation.

Help me fix my Python installation

Create your project folder and file

You need a dedicated folder to keep your expense tracker code organized. You will create this on your Desktop so it is easy to find.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir expense_tracker
cd expense_tracker

What do these commands do?

  • mkdir expense_tracker creates a new folder called expense_tracker on your Desktop.
  • cd expense_tracker moves your terminal into that folder so all future commands run inside it.
  • Now open this folder as your workspace. Click File in the VS Code menu bar.
  • Click Open Folder.
  • Navigate to Desktop, select the expense_tracker folder, and click Open.

Seeing a trust prompt?

VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.

  • In the Explorer sidebar on the left, hover over the folder name and click the New File icon (a page with a plus sign).
  • Type expense_tracker.py as the filename and press Enter.
  • Confirm the file exists by running:
ls

You should see expense_tracker.py in the output.

Don't see the file?

Make sure your terminal is inside the expense_tracker folder. If VS Code opened a new terminal when you opened the folder, it should already be in the right place.

If the terminal shows a different directory, open a fresh terminal by clicking Terminal in the menu bar, then New Terminal. VS Code automatically opens it inside your workspace folder.

Help me find my file in the terminal

Test with a hello world

A quick print statement confirms that Python can find and run your file. This is your proof that the full toolchain (editor, terminal, Python) is connected.

  • Click on expense_tracker.py in the Explorer sidebar to open it in the editor.
  • Type the following code into the file:
print("Expense Tracker - Ready!")

What does this code do?

print() is a built-in Python function that outputs text to the terminal. If this runs without errors, it proves Python can read and execute your file.

  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Run the file by entering this command in your terminal:
python expense_tracker.py

You should see Expense Tracker - Ready! printed in the terminal.

Seeing a 'No such file or directory' error?

This means your terminal is not in the same folder as your file. Run ls to see what files are in your current directory. If expense_tracker.py is not listed, navigate to the correct folder.

If you are using Windows and python did not work earlier, remember to use py expense_tracker.py instead.

Help me debug this error

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Your expense_tracker.py should contain exactly this:

print("Expense Tracker - Ready!")

Your workspace is ready. Next up, you will build the interactive menu system that powers your expense tracker.

Set Up Your Project Structure

Your Python environment is set up and working. Now it's time to build the actual expense tracker application.

Every well-structured application separates its concerns into distinct functions. You'll create functions for saving data, recording expenses, and navigating a menu. This keeps your code organized and easy to modify later.

In this step, get ready to:
  • Create data loading and saving functions for persistent storage.
  • Build an expense recording function with category selection and input validation.
  • Wire everything together with an interactive menu loop.
Create the data persistence functions

Your expense tracker needs to save data to a file so expenses survive between sessions. You'll use JSON (JavaScript Object Notation) as the storage format because Python has a built-in module that reads and writes it easily.

  • In VS Code, open expense_tracker.py in the expense_tracker folder.
  • Delete the print("Expense Tracker - Ready!") line.
  • Replace it with the following code that sets up imports and data functions:
import json
import os
from datetime import datetime

# File where all expenses will be stored
DATA_FILE = "expenses.json"


def load_expenses():
    # Check if a saved file exists before trying to read it
    if os.path.exists(DATA_FILE):
        with open(DATA_FILE, "r") as file:
            return json.load(file)
    # First run: no file exists yet, start with empty list
    return []


def save_expenses(expenses):
    # Write the full expenses list to JSON with readable formatting
    with open(DATA_FILE, "w") as file:
        json.dump(expenses, file, indent=4)

What does this code do?

The three imports bring in Python's built-in modules. json handles reading and writing JSON files. os checks whether a file exists on disk. datetime records today's date when you add an expense.

DATA_FILE is a constant that stores the filename in one place. If you ever want to rename the file, you only change it here.

load_expenses() reads saved expenses from the JSON file. If the file doesn't exist (first time running), it returns an empty list. save_expenses() writes the current list to disk with indent=4 so the file is human-readable.

  • Save expense_tracker.py.
  • Run the file in your terminal to confirm there are no syntax errors:
python expense_tracker.py

You should see no output and no errors. The file just defines functions without calling them yet.

Seeing a SyntaxError or IndentationError?

  • Check that every line inside a function is indented with exactly 4 spaces.
  • Make sure you deleted the original print("Expense Tracker - Ready!") line completely.
  • Confirm the from datetime import datetime line uses from...import syntax, not just import datetime.

Help me fix a Python syntax error in my expense tracker imports

Build the add expense function

The core feature of your tracker is recording expenses. This function asks the user to pick a category, enter an amount, and add a description. It validates every input so the program never crashes from bad data.

  • In expense_tracker.py, add the following function below save_expenses:
def add_expense(expenses):
    print("\n--- Add New Expense ---")

    # List of spending categories the user can choose from
    categories = ["Food", "Transport", "Shopping", "Bills", "Entertainment", "Health", "Other"]
    print("\nCategories:")
    for i, category in enumerate(categories, 1):
        print(f"  {i}. {category}")

    # Keep asking until the user enters a valid category number
    while True:
        try:
            choice = int(input("\nSelect category (1-7): "))
            if 1 <= choice <= 7:
                category = categories[choice - 1]
                break
            else:
                print("Please enter a number between 1 and 7.")
        except ValueError:
            print("Invalid input. Please enter a number.")

    # Keep asking until the user enters a valid positive amount
    while True:
        try:
            amount = float(input("Enter amount in Rs: "))
            if amount > 0:
                break
            else:
                print("Amount must be greater than 0.")
        except ValueError:
            print("Invalid input. Please enter a valid number.")

    # Get a description, default to "No description" if left blank
    description = input("Enter description: ").strip()
    if not description:
        description = "No description"

    # Record today's date automatically
    date = datetime.now().strftime("%Y-%m-%d")

    # Build the expense as a dictionary
    expense = {
        "date": date,
        "category": category,
        "amount": amount,
        "description": description
    }

    # Add to the list and save immediately
    expenses.append(expense)
    save_expenses(expenses)
    print(f"\nExpense added: Rs {amount:.2f} for {category} ({description})")

What does this code do?

  • categories is a list of 7 spending categories. The enumerate(categories, 1) loop prints them as a numbered menu starting from 1.
  • The while True loops with try/except ValueError keep asking until the user provides valid input. If someone types letters instead of a number, the program shows an error message and asks again instead of crashing.
  • datetime.now().strftime("%Y-%m-%d") captures today's date in a clean format like 2026-06-16.
  • The expense is stored as a dictionary with four keys: date, category, amount, and description. Then it's appended to the list and saved to disk immediately.

Why validate input with while loops?

Without validation, typing "abc" when the program expects a number would crash the entire app with a ValueError. The while True pattern traps the error and politely asks again. The loop only breaks when valid data arrives.

  • Save expense_tracker.py.
  • Run the file again to confirm no errors appear:
python expense_tracker.py

You should see no output and no errors. The function is defined but nothing calls it yet.

Seeing a NameError or IndentationError?

  • Make sure there are two blank lines between save_expenses and add_expense. Python expects consistent spacing between top-level functions.
  • Check that all code inside the function is indented 4 spaces, and code inside while True is indented 8 spaces.
  • Confirm the function starts with def add_expense(expenses): with a colon at the end.

Help me fix an error in my add_expense function

Create the main menu loop

The menu is the entry point of your application. It displays options, reads the user's choice, and calls the right function. A while True loop keeps it running until the user explicitly chooses to exit.

  • Add the following code at the bottom of expense_tracker.py, below the add_expense function:
def main():
    print("=" * 40)
    print("       EXPENSE TRACKER (Rs)")
    print("=" * 40)

    # Load any previously saved expenses
    expenses = load_expenses()

    while True:
        print("\n--- Main Menu ---")
        print("1. Add Expense")
        print("2. View All Expenses")
        print("3. Category Summary")
        print("4. Filter by Category")
        print("5. Exit")

        choice = input("\nSelect an option (1-5): ").strip()

        # Route the user's choice to the correct function
        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            view_expenses(expenses)
        elif choice == "3":
            category_summary(expenses)
        elif choice == "4":
            filter_by_category(expenses)
        elif choice == "5":
            print("\nGoodbye! Your expenses are saved.")
            break
        else:
            print("Invalid option. Please enter 1-5.")


if __name__ == "__main__":
    main()

What does this code do?

  • main() prints a welcome banner, loads existing expenses from disk, then enters the menu loop.
  • The if/elif/else chain routes the user's input to the matching function. Options 2-4 call functions you'll build in the next steps.
  • if __name__ == "__main__": is a Python convention that means "only run main() when this file is executed directly." It prevents the menu from launching if the file is imported as a module.

Options 2-4 will error for now

The menu references view_expenses, category_summary, and filter_by_category which don't exist yet. Selecting those options will show a NameError. That's expected. You'll build these functions in upcoming steps.

  • Save expense_tracker.py.
  • Run your expense tracker by executing this command:
python expense_tracker.py

You should see the welcome banner and the main menu with 5 options.

  • Type 1 and press **Enter** to select **Add Expense**.
  • Select a category by typing a number between 1 and 7.
  • Enter an amount like 150.
  • Enter a description like Lunch at canteen.

You should see a confirmation message like Expense added: Rs 150.00 for Food (Lunch at canteen) and the menu should reappear.

  • Try typing letters (like abc) when it asks for a category or amount to see input validation in action.
  • Type 5 to exit the program.
  • Check your project folder. You should see a new expenses.json file that was automatically created when you added your expense.

Menu doesn't appear or the program crashes?

  • Make sure if __name__ == "__main__": is at the very bottom of the file with no indentation.
  • Confirm that main() is indented under the if __name__ block.
  • If you see a NameError mentioning add_expense, make sure the function definition appears above main() in the file.

Help me debug my expense tracker menu

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete expense_tracker.py at this point. Compare it with yours and fix any differences.

import json
import os
from datetime import datetime

DATA_FILE = "expenses.json"


def load_expenses():
    if os.path.exists(DATA_FILE):
        with open(DATA_FILE, "r") as file:
            return json.load(file)
    return []


def save_expenses(expenses):
    with open(DATA_FILE, "w") as file:
        json.dump(expenses, file, indent=4)


def add_expense(expenses):
    print("\n--- Add New Expense ---")

    categories = ["Food", "Transport", "Shopping", "Bills", "Entertainment", "Health", "Other"]
    print("\nCategories:")
    for i, category in enumerate(categories, 1):
        print(f"  {i}. {category}")

    while True:
        try:
            choice = int(input("\nSelect category (1-7): "))
            if 1 <= choice <= 7:
                category = categories[choice - 1]
                break
            else:
                print("Please enter a number between 1 and 7.")
        except ValueError:
            print("Invalid input. Please enter a number.")

    while True:
        try:
            amount = float(input("Enter amount in Rs: "))
            if amount > 0:
                break
            else:
                print("Amount must be greater than 0.")
        except ValueError:
            print("Invalid input. Please enter a valid number.")

    description = input("Enter description: ").strip()
    if not description:
        description = "No description"

    date = datetime.now().strftime("%Y-%m-%d")

    expense = {
        "date": date,
        "category": category,
        "amount": amount,
        "description": description
    }

    expenses.append(expense)
    save_expenses(expenses)
    print(f"\nExpense added: Rs {amount:.2f} for {category} ({description})")


def main():
    print("=" * 40)
    print("       EXPENSE TRACKER (Rs)")
    print("=" * 40)

    expenses = load_expenses()

    while True:
        print("\n--- Main Menu ---")
        print("1. Add Expense")
        print("2. View All Expenses")
        print("3. Category Summary")
        print("4. Filter by Category")
        print("5. Exit")

        choice = input("\nSelect an option (1-5): ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            view_expenses(expenses)
        elif choice == "3":
            category_summary(expenses)
        elif choice == "4":
            filter_by_category(expenses)
        elif choice == "5":
            print("\nGoodbye! Your expenses are saved.")
            break
        else:
            print("Invalid option. Please enter 1-5.")


if __name__ == "__main__":
    main()

Your expense tracker can now record spending and save it to disk. Next up, you'll build the functions that let you view and analyze your expenses.

Save and Load Data with JSON

In the previous step, you built a working menu system and an add_expense function that collects user input with validation.

Your expenses list currently lives in memory. The moment you close the terminal, everything disappears. Two functions in your file handle reading and writing data to a JSON file so your expenses survive between sessions. Let's understand how they work and verify the persistence in action.

In this step, get ready to:
  • Understand how your save function writes expenses to a JSON file.
  • Understand how your load function reads expenses back on startup.
  • Verify that expense data persists between sessions.
Understand the save_expenses function

When you add an expense, the app needs to write all your data to a file on disk. This is what save_expenses() handles.

  • In expense_tracker.py, locate the save_expenses function near the top of your file:
def save_expenses(expenses):
    with open(DATA_FILE, "w") as file:
        json.dump(expenses, file, indent=4)

What does this function do?

This function takes the full expenses list and writes it to expenses.json every time it is called.

  • open(DATA_FILE, "w") opens the file in write mode. The "w" means it overwrites the file each time with the full updated list.
  • json.dump(expenses, file, indent=4) converts your Python list of dictionaries into JSON text and writes it to the file. The indent=4 makes the output human-readable with nice spacing.

Why overwrite the whole file?

Instead of appending one expense at a time, the function writes the entire list on every save. This keeps the file in a valid JSON format and avoids corruption issues that come from partial writes.

Understand the load_expenses function

The other half of persistence is reading that file back in when the program starts. This is where load_expenses() comes in.

  • Find the load_expenses function just above save_expenses:
def load_expenses():
    if os.path.exists(DATA_FILE):
        with open(DATA_FILE, "r") as file:
            return json.load(file)
    return []

What does this function do?

  • os.path.exists(DATA_FILE) checks whether expenses.json exists on disk. On the very first run, it will not exist yet.
  • If the file exists, json.load(file) reads the JSON text and converts it back into a Python list of dictionaries.
  • If the file does not exist, the function returns an empty list ([]) so the program can start fresh without crashing.
Verify how saving and loading connect to the app

These two functions work together through your main() and add_expense() functions. Let's trace the data flow.

  • In your main() function, find this line near the top:
    expenses = load_expenses()
  • Now in your add_expense() function, find this line near the bottom:
    save_expenses(expenses)

How does the data flow work?

When the program starts, main() calls load_expenses() to populate the list from the file. When the learner adds an expense, add_expense() calls save_expenses() to write everything back to disk immediately.

This means no data is lost even if the program crashes unexpectedly. Every single expense is persisted the moment it is added.

Now let's prove that persistence actually works by running the app twice.

  • Run your expense tracker by entering this command in the terminal:
python expense_tracker.py
  • Select option 1 to add an expense.
  • Pick any category (e.g. 1 for Food).
  • Enter an amount like 150.
  • Enter a description like Lunch.
  • Select option 5 to exit the program.

You should see the goodbye message printed in the terminal.

  • Check that the JSON file was created by running:
ls

You should see expense_tracker.py and expenses.json listed in the output.

  • Now run the program again:
python expense_tracker.py
  • Select option 1 again and add a second expense with a different category and amount.
  • Select option 5 to exit.

What should I see?

When you ran the program the second time, the expense you added in the first session was still stored in memory. Both the first and second expense are now saved in expenses.json.

  • Open expenses.json in VS Code by clicking on it in the Explorer sidebar.

You should see a neatly formatted JSON array containing both of your expenses, each with date, category, amount, and description fields.

Not seeing expenses.json?

  • Make sure you actually added an expense before exiting. The file is only created after the first save.
  • Check that your terminal is inside the expense_tracker folder. If you ran the script from a different location, the JSON file may have been created there instead.
  • Verify that DATA_FILE = "expenses.json" appears near the top of your file, right after the import statements.

My expenses.json file is not appearing after I run the program.

Your expenses now survive between sessions. Next up, you will build functions to view all your expenses in a formatted table and calculate spending totals.

View Expenses and Calculate Totals

Your expenses now persist between sessions thanks to JSON file handling. You can add expenses, close the program, and pick up right where you left off.

Right now, though, you have no way to actually see your saved expenses. In this step, you will build a function that displays all your expenses in a neatly aligned table with a running total at the bottom.

In this step, get ready to:
  • Build the view_expenses function with formatted table output.
  • Handle the empty state so the program does not crash with no data.
Build the view_expenses function

Python f-strings support alignment formatting. The syntax f"{value:<12}" left-aligns a value inside a 12-character-wide column. This keeps your table columns lined up regardless of how long each value is.

  • In expense_tracker.py, find the add_expense function. Place your cursor after the last line of that function (the print line that confirms the expense was added), then add a blank line and paste the following code:
def view_expenses(expenses):
    print("\n--- All Expenses ---")

    if not expenses:
        print("No expenses recorded yet.")
        return

    print(f"\n{'Date':<12} {'Category':<15} {'Amount (Rs)':<12} {'Description'}")
    print("-" * 55)

    total = 0
    for expense in expenses:
        print(f"{expense['date']:<12} {expense['category']:<15} {expense['amount']:<12.2f} {expense['description']}")
        total += expense["amount"]

    print("-" * 55)
    print(f"{'TOTAL':<27} Rs {total:.2f}")

What does this code do?

  • if not expenses: checks whether the expenses list is empty. If it is, the function prints a friendly message and exits early with return so the rest of the code does not run on an empty list.
  • The header line uses f-string alignment (<12, <15) to set column widths. Each expense row uses the same widths so everything lines up.
  • The for loop prints each expense and adds its amount to total. After the loop, the total is displayed at the bottom of the table.
  • :.2f formats numbers to exactly 2 decimal places. So Rs 50 displays as Rs 50.00.
  • Save expense_tracker.py.

Seeing a syntax error?

  • Make sure the function definition starts at the same indentation level as def add_expense above it. It should not be indented inside another function.
  • Check that the f-string curly braces match. Each { needs a closing }. The dictionary access inside uses single quotes (expense['date']) while the outer string uses double quotes.

Why is my view_expenses function not working?

Test the empty state and formatted output

The if not expenses: guard at the top of the function is important. Without it, the program would print an empty table with just headers and a zero total. The early return gives a cleaner experience.

  • If you have an existing expenses.json file from earlier testing, delete it temporarily so you can test the empty state. Run the program:
python expense_tracker.py
  • Select option 2 (View All Expenses). You should see the message "No expenses recorded yet." and return to the menu.
  • Now select option 1 and add 2-3 expenses with different categories and amounts.
  • Select option 2 again. You should see a neatly formatted table with aligned columns showing Date, Category, Amount (Rs), and Description, plus a total at the bottom.

What should I see?

Your output should look something like this (with today's date and your own values):

--- All Expenses ---

Date         Category        Amount (Rs)  Description
-------------------------------------------------------
2026-06-16   Food            150.00       Lunch
2026-06-16   Transport       50.00        Bus fare
-------------------------------------------------------
TOTAL                       Rs 200.00

The columns should be aligned so that each row lines up neatly under its header.

Columns not lining up?

  • Check that the alignment numbers in your header (<12, <15) match the alignment numbers in the loop body. They must be identical for rows to align with headers.
  • If you see a NameError: name 'view_expenses' is not defined, make sure your new function is placed ABOVE the main() function. Python reads top to bottom, so the function must be defined before it is called.

Help me fix my view_expenses table alignment.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete expense_tracker.py file up to this point. Compare yours against it:

import json
import os
from datetime import datetime

DATA_FILE = "expenses.json"


def load_expenses():
    if os.path.exists(DATA_FILE):
        with open(DATA_FILE, "r") as file:
            return json.load(file)
    return []


def save_expenses(expenses):
    with open(DATA_FILE, "w") as file:
        json.dump(expenses, file, indent=4)


def add_expense(expenses):
    print("\n--- Add New Expense ---")

    categories = ["Food", "Transport", "Shopping", "Bills", "Entertainment", "Health", "Other"]
    print("\nCategories:")
    for i, category in enumerate(categories, 1):
        print(f"  {i}. {category}")

    while True:
        try:
            choice = int(input("\nSelect category (1-7): "))
            if 1 <= choice <= 7:
                category = categories[choice - 1]
                break
            else:
                print("Please enter a number between 1 and 7.")
        except ValueError:
            print("Invalid input. Please enter a number.")

    while True:
        try:
            amount = float(input("Enter amount in Rs: "))
            if amount > 0:
                break
            else:
                print("Amount must be greater than 0.")
        except ValueError:
            print("Invalid input. Please enter a valid number.")

    description = input("Enter description: ").strip()
    if not description:
        description = "No description"

    date = datetime.now().strftime("%Y-%m-%d")

    expense = {
        "date": date,
        "category": category,
        "amount": amount,
        "description": description
    }

    expenses.append(expense)
    save_expenses(expenses)
    print(f"\nExpense added: Rs {amount:.2f} for {category} ({description})")


def view_expenses(expenses):
    print("\n--- All Expenses ---")

    if not expenses:
        print("No expenses recorded yet.")
        return

    print(f"\n{'Date':<12} {'Category':<15} {'Amount (Rs)':<12} {'Description'}")
    print("-" * 55)

    total = 0
    for expense in expenses:
        print(f"{expense['date']:<12} {expense['category']:<15} {expense['amount']:<12.2f} {expense['description']}")
        total += expense["amount"]

    print("-" * 55)
    print(f"{'TOTAL':<27} Rs {total:.2f}")


def main():
    print("=" * 40)
    print("       EXPENSE TRACKER (Rs)")
    print("=" * 40)

    expenses = load_expenses()

    while True:
        print("\n--- Main Menu ---")
        print("1. Add Expense")
        print("2. View All Expenses")
        print("3. Category Summary")
        print("4. Filter by Category")
        print("5. Exit")

        choice = input("\nSelect an option (1-5): ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            view_expenses(expenses)
        elif choice == "3":
            category_summary(expenses)
        elif choice == "4":
            filter_by_category(expenses)
        elif choice == "5":
            print("\nGoodbye! Your expenses are saved.")
            break
        else:
            print("Invalid option. Please enter 1-5.")


if __name__ == "__main__":
    main()

You can now see exactly where your money is going. Next up, you will build category summaries and filtering so you can analyze your spending patterns at a glance.

Build Category Summary and Filtering

You can now add expenses and view them in a neatly formatted table. The tracker records data and persists it between sessions.

Tracking expenses is only half the story. The real value comes from understanding where your money goes. In this step, you'll build two analytical functions that turn raw data into actionable insights.

In this step, get ready to:
  • Build a category summary that shows spending percentages.
  • Build a filter that isolates expenses by category.
  • Test the complete application end to end.
Build the category summary function

The category summary needs to answer one question: how much did you spend in each category, and what percentage of your total spending does that represent? A dictionary is the perfect data structure for accumulating totals per category.

  • In expense_tracker.py, find the view_expenses function. Add the following category_summary function below it:
def category_summary(expenses):
    print("\n--- Category Summary ---")

    if not expenses:
        print("No expenses recorded yet.")
        return

    # Build a dictionary of category totals
    summary = {}
    for expense in expenses:
        category = expense["category"]
        if category in summary:
            summary[category] += expense["amount"]
        else:
            summary[category] = expense["amount"]

    total = sum(summary.values())

    # Display sorted table from highest to lowest spending
    print(f"\n{'Category':<15} {'Amount (Rs)':<12} {'Percentage'}")
    print("-" * 40)

    for category, amount in sorted(summary.items(), key=lambda x: x[1], reverse=True):
        percentage = (amount / total) * 100
        print(f"{category:<15} {amount:<12.2f} {percentage:.1f}%")

    print("-" * 40)
    print(f"{'TOTAL':<15} Rs {total:.2f}")

What does this code do?

  • The summary dictionary accumulates spending per category. If a category already exists as a key, its value increases. If not, the category is added with the current amount.
  • sorted(summary.items(), key=lambda x: x[1], reverse=True) sorts categories by their total amount from highest to lowest, so the biggest spending areas appear first.
  • The percentage calculation divides each category's total by the overall total, then multiplies by 100.
  • Save expense_tracker.py.
  • Run your program, add 3 or more expenses across different categories (if you have not already), then select option 3 (Category Summary). You should see a table with each category's total and percentage, sorted from highest to lowest.

Seeing a NameError for category_summary?

Make sure the function is defined above the main() function. Python reads top to bottom, so functions must be defined before they are called.

Also check that the function name is spelled exactly category_summary with an underscore, matching the call in your menu.

Why is my category_summary function not being found?

Build the filter by category function

The category summary gives you the big picture. The filter function lets you drill into a specific category to see each individual expense and its subtotal.

  • In expense_tracker.py, add the following filter_by_category function below category_summary:
def filter_by_category(expenses):
    print("\n--- Filter by Category ---")

    if not expenses:
        print("No expenses recorded yet.")
        return

    # Show the category menu
    categories = ["Food", "Transport", "Shopping", "Bills", "Entertainment", "Health", "Other"]
    print("\nCategories:")
    for i, category in enumerate(categories, 1):
        print(f"  {i}. {category}")

    # Validate the user's category choice
    while True:
        try:
            choice = int(input("\nSelect category to filter (1-7): "))
            if 1 <= choice <= 7:
                selected = categories[choice - 1]
                break
            else:
                print("Please enter a number between 1 and 7.")
        except ValueError:
            print("Invalid input. Please enter a number.")

    # Filter expenses matching the selected category
    filtered = [e for e in expenses if e["category"] == selected]

    if not filtered:
        print(f"\nNo expenses found for {selected}.")
        return

    # Display filtered results with subtotal
    print(f"\n--- Expenses for {selected} ---")
    print(f"\n{'Date':<12} {'Amount (Rs)':<12} {'Description'}")
    print("-" * 40)

    total = 0
    for expense in filtered:
        print(f"{expense['date']:<12} {expense['amount']:<12.2f} {expense['description']}")
        total += expense["amount"]

    print("-" * 40)
    print(f"{'TOTAL':<12} Rs {total:.2f}")

What does this code do?

  • The function reuses the same category menu and input validation pattern from add_expense. Consistent patterns make your code easier to maintain.
  • The list comprehension [e for e in expenses if e["category"] == selected] creates a new list containing only expenses that match the chosen category.
  • If no expenses match the filter, the function prints a friendly message and returns early instead of showing an empty table.
  • Save expense_tracker.py.
  • Run your program and select option 4 (Filter by Category). Pick a category you have added expenses for. You should see only that category's expenses listed with a subtotal at the bottom.

Filter showing no expenses for a category you used?

The filter compares category names exactly. Make sure your categories list matches the one in add_expense (same spelling and capitalization).

If you edited the category names in one function but not the other, the strings will not match and filtering will return empty results.

Why is my filter returning no expenses even though I added some?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on to the final test.

ⓧ I'd like to double check the full code

Here is the complete expense_tracker.py file at this point:

import json
import os
from datetime import datetime

DATA_FILE = "expenses.json"


def load_expenses():
    if os.path.exists(DATA_FILE):
        with open(DATA_FILE, "r") as file:
            return json.load(file)
    return []


def save_expenses(expenses):
    with open(DATA_FILE, "w") as file:
        json.dump(expenses, file, indent=4)


def add_expense(expenses):
    print("\n--- Add New Expense ---")

    categories = ["Food", "Transport", "Shopping", "Bills", "Entertainment", "Health", "Other"]
    print("\nCategories:")
    for i, category in enumerate(categories, 1):
        print(f"  {i}. {category}")

    while True:
        try:
            choice = int(input("\nSelect category (1-7): "))
            if 1 <= choice <= 7:
                category = categories[choice - 1]
                break
            else:
                print("Please enter a number between 1 and 7.")
        except ValueError:
            print("Invalid input. Please enter a number.")

    while True:
        try:
            amount = float(input("Enter amount in Rs: "))
            if amount > 0:
                break
            else:
                print("Amount must be greater than 0.")
        except ValueError:
            print("Invalid input. Please enter a valid number.")

    description = input("Enter description: ").strip()
    if not description:
        description = "No description"

    date = datetime.now().strftime("%Y-%m-%d")

    expense = {
        "date": date,
        "category": category,
        "amount": amount,
        "description": description
    }

    expenses.append(expense)
    save_expenses(expenses)
    print(f"\nExpense added: Rs {amount:.2f} for {category} ({description})")


def view_expenses(expenses):
    print("\n--- All Expenses ---")

    if not expenses:
        print("No expenses recorded yet.")
        return

    print(f"\n{'Date':<12} {'Category':<15} {'Amount (Rs)':<12} {'Description'}")
    print("-" * 55)

    total = 0
    for expense in expenses:
        print(f"{expense['date']:<12} {expense['category']:<15} {expense['amount']:<12.2f} {expense['description']}")
        total += expense["amount"]

    print("-" * 55)
    print(f"{'TOTAL':<27} Rs {total:.2f}")


def category_summary(expenses):
    print("\n--- Category Summary ---")

    if not expenses:
        print("No expenses recorded yet.")
        return

    summary = {}
    for expense in expenses:
        category = expense["category"]
        if category in summary:
            summary[category] += expense["amount"]
        else:
            summary[category] = expense["amount"]

    total = sum(summary.values())

    print(f"\n{'Category':<15} {'Amount (Rs)':<12} {'Percentage'}")
    print("-" * 40)

    for category, amount in sorted(summary.items(), key=lambda x: x[1], reverse=True):
        percentage = (amount / total) * 100
        print(f"{category:<15} {amount:<12.2f} {percentage:.1f}%")

    print("-" * 40)
    print(f"{'TOTAL':<15} Rs {total:.2f}")


def filter_by_category(expenses):
    print("\n--- Filter by Category ---")

    if not expenses:
        print("No expenses recorded yet.")
        return

    categories = ["Food", "Transport", "Shopping", "Bills", "Entertainment", "Health", "Other"]
    print("\nCategories:")
    for i, category in enumerate(categories, 1):
        print(f"  {i}. {category}")

    while True:
        try:
            choice = int(input("\nSelect category to filter (1-7): "))
            if 1 <= choice <= 7:
                selected = categories[choice - 1]
                break
            else:
                print("Please enter a number between 1 and 7.")
        except ValueError:
            print("Invalid input. Please enter a number.")

    filtered = [e for e in expenses if e["category"] == selected]

    if not filtered:
        print(f"\nNo expenses found for {selected}.")
        return

    print(f"\n--- Expenses for {selected} ---")
    print(f"\n{'Date':<12} {'Amount (Rs)':<12} {'Description'}")
    print("-" * 40)

    total = 0
    for expense in filtered:
        print(f"{expense['date']:<12} {expense['amount']:<12.2f} {expense['description']}")
        total += expense["amount"]

    print("-" * 40)
    print(f"{'TOTAL':<12} Rs {total:.2f}")


def main():
    print("=" * 40)
    print("       EXPENSE TRACKER (Rs)")
    print("=" * 40)

    expenses = load_expenses()

    while True:
        print("\n--- Main Menu ---")
        print("1. Add Expense")
        print("2. View All Expenses")
        print("3. Category Summary")
        print("4. Filter by Category")
        print("5. Exit")

        choice = input("\nSelect an option (1-5): ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            view_expenses(expenses)
        elif choice == "3":
            category_summary(expenses)
        elif choice == "4":
            filter_by_category(expenses)
        elif choice == "5":
            print("\nGoodbye! Your expenses are saved.")
            break
        else:
            print("Invalid option. Please enter 1-5.")


if __name__ == "__main__":
    main()
Test the complete application

All four menu options are now functional. Time to run through the full workflow and verify everything works together.

  • Run your expense tracker by running this command:
python expense_tracker.py
  • Add 5 or more expenses across at least 3 different categories (for example, Food, Transport, and Shopping).
  • Select option 2 (View All Expenses) to see the full list with a total at the bottom.
  • Select option 3 (Category Summary) to see each category's total and percentage, sorted from highest to lowest.
  • Select option 4 (Filter by Category) and pick a category. Verify that only expenses from that category appear with the correct subtotal.
  • Select option 5 to exit, then run the program again. Your expenses should still be there, loaded from expenses.json.

What should I see?

The category summary percentages should add up to 100%. The filter should show only expenses from the selected category with a subtotal that is less than or equal to the overall total from View All Expenses.

Percentages not adding up to 100%?

Due to rounding, percentages displayed as one decimal place might show a total of 99.9% or 100.1%. This is normal floating-point behaviour and not a bug in your code.

If the total is significantly off (like 80% or 120%), check that your summary dictionary is accumulating amounts correctly with the += operator.

My category percentages are way off. Can you help me debug?

Secret mission

Add Monthly Budget Tracking

Your expense tracker records spending, but it does not tell you when you are spending too much. In this mission, you will add the ability to set monthly budget limits per category and get automatic warnings when you approach or exceed them.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your machine, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your project files to continue experimenting, or delete them to free up space.

Resources you used:

  • The expense_tracker folder on your Desktop containing expense_tracker.py.
  • The expenses.json file storing your expense data.
  • The budgets.json file storing your budget limits (if you completed the Secret Mission).

Keep everything running

No action needed. Choose this if you want to keep using your expense tracker or continue experimenting with the code.

  • Your expense_tracker folder stays on your Desktop.
  • You can run python expense_tracker.py any time to add more expenses.
  • Your saved expenses and budgets persist in the JSON files.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files sit quietly on disk until you return.

  • Close the expense_tracker.py tab in VS Code.
  • Your data in expenses.json and budgets.json will be waiting when you come back.

Delete - I don't want to use this again

Remove all project files from your machine. This deletes your expense history and budget settings permanently.

  • Close any open files from the project in VS Code.
  • Delete the entire project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/expense_tracker

Windows

rmdir /s /q %USERPROFILE%\Desktop\expense_tracker
  • Confirm the folder is gone by checking your Desktop.

Nice Work!

Nice Work!

You just built a fully functional personal expense tracker in Python from scratch. This is a real application you can use every day to monitor your spending in Rupees.

You've learned how to:

  • Built a complete Python CLI application using functions, loops, dictionaries, and conditional logic to create an interactive menu system.
  • Stored and retrieved data persistently using JSON file handling so expenses survive between sessions.
  • Formatted terminal output into readable aligned tables with category summaries and spending percentages.
  • Secret Mission: Added monthly budget tracking per category with automatic overspending warnings at 80% and 100% thresholds.

Ready to quiz yourself?