Budget Buddy: Python Expense Tracker

Build a terminal expense tracker that saves records to JSON.

Introduction

30 Second Summary

A quick spending note is useful only if it is still there the next time you check it. A program that forgets every entry makes even a tiny budget hard to trust.

In this project, you will build Budget Buddy, a persistent terminal expense tracker in Python that runs inside Visual Studio Code. Your finished tracker keeps a reliable spending history across restarts.

What You'll Build

When you demo Budget Buddy, the terminal shows a formatted expense list with its running total and records saved during earlier sessions.

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

  • A guided expense entry that turns a description, category, and amount into a formatted line in the terminal.
  • A running total that updates as you record several expenses during one session.
  • A persistent spending history stored in the local JSON file expenses.json. Your saved expenses return after a restart. Invalid amounts receive helpful feedback without reaching the file.
  • Secret Mission: Add a case-insensitive category filter that displays matching expenses and their subtotal.

Are there any prerequisites?

A working Python 3 installation and Visual Studio Code are the only prerequisites. The project includes setup for the Python extension for Visual Studio Code if you need it.

Before We Start

This first checkpoint helps you commit to the tracker you are about to build. You will define what Budget Buddy records and why those records must survive after the app closes.

Set Up Your Budget Buddy Workspace

Budget Buddy needs a stable workspace for its script. Visual Studio Code also needs to know which Python installation runs that script.

This step removes that setup uncertainty. You will finish with a saved starter script that prints the app title in the integrated terminal.

In this step, get ready to:
  • Verify the Windows Python launcher.
  • Confirm Python support in Visual Studio Code.
  • Run the Budget Buddy title from expense_tracker.py.
Verify Python in the terminal

The Windows Python launcher connects terminal commands to your existing installation. Printing the installed version proves that connection works before your script depends on it.

  • Return to Visual Studio Code from earlier.
  • Press Ctrl+` to open the integrated terminal.
  • Verify Python by running this command:
py -3 --version

What does this command check?

The py launcher finds your installed interpreter. The -3 option requests Python 3.

The --version option prints the interpreter version. A printed version confirms that your terminal can run Python.

You should see a line beginning with Python followed by the installed version number.

No Python Version Printed?

  • Confirm that the terminal panel belongs to Visual Studio Code.
  • Compare your command with the one above for missing spaces.

Ask for help with the launcher output by selecting help me troubleshoot the Windows Python launcher.

Confirm the Python extension

The Python extension for Visual Studio Code adds language support plus the Run Python File action. Your installed interpreter performs the actual execution.

  • Press Ctrl+Shift+X to open Extensions.
  • Enter @id:ms-python.python in the Extensions search box.
  • Select the Python extension by Microsoft.

✔️ The Extension Is Installed

The extension is already available in Visual Studio Code. No installation action is needed.

ⓧ I See Install

  • Click Install on the extension page.

Wait for the installation to finish. You should see the extension marked as installed.

Extension Installation Stuck?

  • Confirm that your Windows computer has an active internet connection.
  • Retry the installation from the Python extension page.

Ask for targeted help by selecting help me install the Python extension.

Visual Studio Code may ask which interpreter should run Python files. The version you verified in the terminal is the one you want.

  • If Visual Studio Code asks for an interpreter, press Ctrl+Shift+P to open the Command Palette.
  • If the prompt appears, select Python: Select Interpreter.
  • If an interpreter list appears, choose the entry matching the version printed in your terminal.

That is the editor wiring sorted. Visual Studio Code can now run Python files with the installation you verified.

Create and run the starter script

A Visual Studio Code workspace keeps related project files inside one named folder. Your budget-buddy workspace starts with one script that proves the complete run path works.

  • Click File in the Visual Studio Code menu.
  • Select Open Folder....
  • Navigate to your Desktop in the folder picker.
  • Select New Folder.
  • Enter budget-buddy as the folder name.
  • Select the new budget-buddy folder.
  • Click Select Folder.
  • If Workspace Trust appears, click Yes, I trust the authors for the folder you created.

You should see budget-buddy at the top of the Explorer view. This confirms that Visual Studio Code opened the intended workspace.

  • Select Explorer in the Activity Bar.
  • Select New File....
  • Enter expense_tracker.py as the file name.
  • Press Enter.
  • Add the starter title to the empty editor by using this code:
print("Budget Buddy")

What Does This Code Do?

The built-in print function writes the text between the quotation marks to the terminal. This gives you an immediate signal that the script reached Python successfully.

  • Press Ctrl+S to save expense_tracker.py.

Before you run the script, what title do you expect the integrated terminal to display?

  • Click the Run Python File button in the top-right corner of the editor.

You should see Budget Buddy in the integrated terminal. Your workspace can now save a Python file and run it with the selected interpreter.

Budget Buddy Missing From the Terminal?

  • Confirm that expense_tracker.py is the active editor tab.
  • Confirm that the file contains the same capitalization shown above.
  • Save the file again before selecting Run Python File.

Ask for help with the run result by selecting help me run my Budget Buddy starter script.

✔️ Awesome, I've Got Everything

Great work. Confirm that expense_tracker.py is saved inside budget-buddy before moving on.

ⓧ I'd Like to Double Check the Full Code

Compare your complete expense_tracker.py file with this starter version:

print("Budget Buddy")

What Should the Complete File Do?

This one-line file is the complete starter state for this step. It gives the next step a saved working baseline.

Your workspace now runs Budget Buddy. Next, you will turn the title into a script that records its first expense.

Record Your First Expense

Your Python workspace is ready. The starter script proves that Budget Buddy runs in the integrated terminal.

An expense tracker needs to turn separate terminal answers into one structured expense. This first version records one item before the script exits.

In this step, get ready to:
  • Collect each expense detail from a terminal prompt.
  • Store the answers as one structured expense.
  • Display the expense with a two-decimal amount.
Collect the expense details

The input() function pauses the script for one answer. The strip() method removes surrounding whitespace from that answer.

Every answer starts as a string. The float() function converts the amount into a number.

  • In expense_tracker.py, select the existing print("Budget Buddy") line.
  • Replace that line with three prompts by pasting this code:
description = input("Description: ").strip()
category = input("Category: ").strip()
amount = float(input("Amount: $").strip())

What Does This Code Do?

  • The description variable stores the text from the first prompt.
  • The category variable stores the text from the second prompt.
  • The amount variable stores the numeric result from the final prompt.
  • Save expense_tracker.py.
  • Select Run Python File in the top-right editor toolbar.
  • Enter Lunch at the Description: prompt.
  • Enter Food at the Category: prompt.
  • Enter 8.50 at the Amount: $ prompt.

The integrated terminal accepts all three answers. It returns to the terminal prompt after the amount.

Does the Script Stop After the Amount?

  • Rerun the file with 8.50 at the Amount: $ prompt.
  • Check that all three input lines begin at the left edge of expense_tracker.py.

Ask for help with the input code:

Group the values in a dictionary

A Python dictionary groups related values under named keys. One expense dictionary keeps the three answers together.

  • In expense_tracker.py, place your cursor on the blank line below the amount assignment.
  • Add the expense dictionary by pasting this code:
expense = {
    "description": description,
    "category": category,
    "amount": amount,
}

How Does the Dictionary Work?

  • The expense variable holds one complete expense record.
  • Each string key names one part of the record.
  • Each value points to an answer collected by the prompts.
  • Save expense_tracker.py.
  • Select Run Python File in the top-right editor toolbar.
  • Enter Lunch at the first prompt.
  • Enter Food at the second prompt.
  • Enter 8.50 at the final prompt.

The script collects all three answers without showing a traceback. The integrated terminal returns to its prompt.

Seeing a Dictionary Error?

  • Check that the opening { appears after expense =.
  • Keep a comma after each key-value pair.
  • Confirm that the closing } begins at the left edge of the file.

Ask for help with the dictionary:

Print a formatted expense summary

A format string controls the order of the displayed values. The ${:.2f} placeholder displays the amount with exactly two decimal places.

  • In expense_tracker.py, place your cursor on the blank line below the expense dictionary.
  • Add the formatted summary by pasting this code:
print("{} | {} | ${:.2f}".format(
    expense["description"],
    expense["category"],
    expense["amount"],
))

How Does the Summary Work?

  • The first {} placeholder receives the description.
  • The second {} placeholder receives the category.
  • The ${:.2f} placeholder adds the dollar sign. It fixes the amount at two decimal places.
  • The expressions expense["description"], expense["category"], and expense["amount"] retrieve the stored values.
  • Save expense_tracker.py.

Before you run the finished script, what do you expect the final line to look like after entering 8.50?

  • Select Run Python File in the top-right editor toolbar.
  • Enter Lunch at the Description: prompt.
  • Enter Food at the Category: prompt.
  • Enter 8.50 at the Amount: $ prompt.

You will see Lunch | Food | $8.50 in the integrated terminal. Great work. Budget Buddy now turns terminal answers into a structured expense.

Is the Summary Missing or Misformatted?

  • Compare the three dictionary key names with the keys used in the print() code.
  • Confirm that the amount placeholder is exactly ${:.2f}.
  • Check that the final line ends with )).

Ask for help with the formatted output:

✔️ Awesome, I've got everything!

Your expense_tracker.py file is saved. Your first formatted expense is ready.

ⓧ I'd like to double check the full code

The complete expense_tracker.py file appears below for a character-by-character comparison:

description = input("Description: ").strip()
category = input("Category: ").strip()
amount = float(input("Amount: $").strip())

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

print("{} | {} | ${:.2f}".format(
    expense["description"],
    expense["category"],
    expense["amount"],
))

Your tracker can now collect one expense. Next, you will keep the program running so it can manage several expenses and calculate their total.

Build a Menu and Running Total

Your one-expense script now turns terminal answers into a structured dictionary. The program still exits after that single record.

Functions will package the repeated behavior while a menu keeps one session running. This menu also gives you a concrete test of what the tracker remembers after it closes.

In this step, get ready to:
  • Refactor the one-expense script into reusable functions.
  • Use a menu to collect several expenses in one session.
  • Display a running total before restarting the tracker.
Refactor one expense into reusable functions

A function gives one job a name so the program can run that job whenever the menu requests it. Both functions receive the same expenses list, which lets each new dictionary join the current session.

  • In expense_tracker.py, press Cmd+A (macOS) or Ctrl+A (Windows) to select the existing script.
  • Replace the selected script with the add_expense(expenses) function by pasting this code:
def add_expense(expenses):
    description = input("Description: ").strip()
    category = input("Category: ").strip()
    amount = float(input("Amount: $").strip())

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

What Does This Function Do?

  • The expenses parameter receives the list used by the current session.
  • The three input() calls collect the fields for one expense.
  • The expense dictionary keeps those fields together as one record.
  • The append() call adds that record to the end of the shared list.
  • Save expense_tracker.py with Cmd+S (macOS) or Ctrl+S (Windows).

You'll see the unsaved indicator disappear from the expense_tracker.py tab.

Seeing Indentation Warnings?

  • Confirm every line inside add_expense(expenses) begins with four spaces.
  • Confirm the dictionary lines have eight spaces because they sit inside the function.

Help me fix the indentation in my add_expense function.

The second function turns the current list into readable rows. It also uses sum() to calculate the running total from every stored amount.

  • Place your cursor on a new line below add_expense(expenses).
  • Add list_expenses(expenses) by pasting this code:
def list_expenses(expenses):
    if not expenses:
        print("No expenses recorded.")
        return

    print("\nExpenses")
    for expense in expenses:
        print(
            "- {} | {} | ${:.2f}".format(
                expense["description"],
                expense["category"],
                expense["amount"],
            )
        )

    total = sum(expense["amount"] for expense in expenses)
    print("Total: ${:.2f}".format(total))

How Does Listing Work?

  • The empty-list check prints a helpful message before leaving the function.
  • The for loop visits each expense dictionary in the list.
  • The {:.2f} format displays every amount with two decimal places.
  • The total variable holds the result produced by sum().
  • Save expense_tracker.py.

You'll see the unsaved indicator disappear again. Your file now contains two reusable jobs for the menu.

Is the Second Function Nested?

  • Align def list_expenses(expenses): with def add_expense(expenses): at the left edge of the file.
  • Leave two blank lines between the functions so their boundaries stay clear.

Help me separate my two expense functions.

Add the menu loop

A loop can display the menu again after each choice. The main() function creates the shared list before that loop begins.

  • Place your cursor on a new line below list_expenses(expenses).
  • Add the menu loop plus the script entry point by pasting this code:
def main():
    expenses = []

    while True:
        print("\nBudget Buddy")
        print("1. Add expense")
        print("2. List expenses")
        print("3. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            list_expenses(expenses)
        elif choice == "3":
            print("Goodbye!")
            break
        else:
            print("Choose 1, 2, or 3.")


if __name__ == "__main__":
    main()

How Does the Menu Stay Running?

  • The expenses variable starts as an empty list for this run.
  • The while True loop keeps returning to the menu.
  • The choice checks direct each menu option to its matching function.
  • The __name__ == "__main__" guard calls main() when you run the file directly.

Before you run the completed file, make a quick prediction about the first screen it should show.

  • Save expense_tracker.py.
  • Click the Run Python File button in the top-right corner of Visual Studio Code.

You'll see the Budget Buddy title followed by three numbered choices. The terminal waits at Choose an option:.

  • Enter 1 at the menu prompt.
  • Enter Lunch at the description prompt.
  • Enter Food at the category prompt.
  • Enter 8.50 at the amount prompt.
  • Enter 2 when the menu returns.

You'll see - Lunch | Food | $8.50 followed by Total: $8.50.

You have the repeatable flow working. Budget Buddy can now add a record before returning to the same menu.

Menu Not Appearing?

  • Confirm the if __name__ == "__main__": line starts at the left edge of the file.
  • Confirm main() has four spaces beneath the guard.
  • Confirm both function names in the menu exactly match their definitions.

Help me debug why the Budget Buddy menu does not appear.

✔️ Awesome, I've got everything!

Great. Double check that expense_tracker.py is saved before testing several expenses.

ⓧ I'd like to double check the full code

Here is the complete expense_tracker.py file for comparison:

def add_expense(expenses):
    description = input("Description: ").strip()
    category = input("Category: ").strip()
    amount = float(input("Amount: $").strip())

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


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

    print("\nExpenses")
    for expense in expenses:
        print(
            "- {} | {} | ${:.2f}".format(
                expense["description"],
                expense["category"],
                expense["amount"],
            )
        )

    total = sum(expense["amount"] for expense in expenses)
    print("Total: ${:.2f}".format(total))


def main():
    expenses = []

    while True:
        print("\nBudget Buddy")
        print("1. Add expense")
        print("2. List expenses")
        print("3. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            list_expenses(expenses)
        elif choice == "3":
            print("Goodbye!")
            break
        else:
            print("Choose 1, 2, or 3.")


if __name__ == "__main__":
    main()
Test the total and expose memory loss

Every call to add_expense(expenses) changes the same list during this running session. Listing the expenses again gives you a direct check of the accumulated total.

  • Enter 1 at the menu prompt.
  • Enter Bus at the description prompt.
  • Enter Travel at the category prompt.
  • Enter 2.00 at the amount prompt.
  • Enter 2 when the menu returns.

You'll see both expense rows. The last line reads Total: $10.50.

That total proves both dictionaries reached the same list. The menu has kept your records together throughout this run.

  • Enter 3 at the menu prompt.

You'll see Goodbye! before the program returns control to the integrated terminal.

Before you restart Budget Buddy, make a quick prediction about whether the two expense rows will still be available.

  • Click the Run Python File button again.
  • Enter 2 at the fresh menu prompt.

You'll see No expenses recorded. The two expenses disappeared when the previous run ended.

Why Did the List Reset?

The expenses = [] line creates a new empty list every time main() starts. The dictionaries currently exist only in the program's temporary in-memory state.

This restart result is the intended limitation of the current tracker. You have now proved exactly what the next improvement needs to solve.

Your menu can collect several expenses before calculating their total. Next, you'll turn the restart gap into data that survives after Budget Buddy closes.

Make Expenses Survive a Restart

Your menu can now add several expenses. It can also calculate a running total.

The empty list you saw after restarting exposed the limits of temporary memory. This step uses JSON to keep the complete expenses list on disk.

In this step, get ready to:
  • Load saved expenses when Budget Buddy starts.
  • Save the expenses list after each valid addition.
  • Restart Budget Buddy to prove that an expense persists.
Load saved expenses when the app starts

The menu needs a starting list before it can accept a choice. The first launch has no expenses.json file.

The load_expenses() function handles both starting states. It loads saved records when the file exists.

  • In expense_tracker.py, place the following block above def add_expense(expenses)::
import json

DATA_FILE = "expenses.json"


def load_expenses():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []

What Does This Code Do?

  • The import json line makes Python's JSON tools available.
  • The DATA_FILE constant stores the local data file name.
  • The json.load() call rebuilds the expenses list from the saved file.
  • The FileNotFoundError handler supplies an empty list on the first run.
  • In main(), find the line expenses = [].
  • Replace that line with the following line:
    expenses = load_expenses()

Why Change the Starting List?

The expenses variable now starts with records returned by load_expenses(). A missing file still produces the empty list that the menu expects.

  • Save expense_tracker.py.
  • Select the Run Python File button in the editor toolbar.
  • Enter 2 at the Choose an option: prompt.

You will see No expenses recorded. The missing file was handled without interrupting the menu.

App Not Reaching the Menu?

  • Check that import json is the first line in expense_tracker.py.
  • Confirm that the lines inside load_expenses() use the indentation shown above.

help me debug the Budget Buddy loading code.

  • Enter 3 at the menu prompt to quit.

You will see Goodbye! in the terminal.

Save every valid expense to JSON

Loading can only restore records that were written to disk earlier. The app needs to serialize the updated list immediately after expenses.append(expense).

  • In expense_tracker.py, locate def add_expense(expenses):.
  • Replace the current function with the following block to add saving directly above it:
def save_expenses(expenses):
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(expenses, file, indent=2)


def add_expense(expenses):
    description = input("Description: ").strip()
    category = input("Category: ").strip()
    amount = float(input("Amount: $").strip())

    expense = {
        "description": description,
        "category": category,
        "amount": amount,
    }
    expenses.append(expense)
    save_expenses(expenses)
    print("Expense saved.")

How Does Saving Work?

  • The save_expenses() function opens expenses.json in write mode.
  • The json.dump() call serializes the complete expenses list with readable indentation.
  • The add_expense() function saves the list after appending the new dictionary.
  • The Expense saved. message confirms that the updated list reached the saving step.
  • Save expense_tracker.py.
  • Select the Run Python File button in the editor toolbar.
  • Enter 1 at the menu prompt.
  • Enter a description that you will recognize after restarting.
  • Enter a category that you will recognize after restarting.
  • Enter 4.25 at the Amount: $ prompt.

You will see Expense saved. in the terminal.

  • Confirm that expenses.json is listed beside expense_tracker.py in the Explorer sidebar.

The new file contains the complete expenses list. Budget Buddy now has data it can reload during its next run.

Do Not See the Data File?

  • Confirm that the terminal printed Expense saved. after you entered the amount.
  • Check that save_expenses(expenses) sits directly below expenses.append(expense).
  • Save expense_tracker.py before running it again.

help me find why Budget Buddy is not creating expenses.json.

✔️ Awesome, I've got everything!

Great. Double check that you saved expense_tracker.py before testing the restart.

ⓧ I'd like to double check the full code

Compare your complete expense_tracker.py file with this reference:

import json

DATA_FILE = "expenses.json"


def load_expenses():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []


def save_expenses(expenses):
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(expenses, file, indent=2)


def add_expense(expenses):
    description = input("Description: ").strip()
    category = input("Category: ").strip()
    amount = float(input("Amount: $").strip())

    expense = {
        "description": description,
        "category": category,
        "amount": amount,
    }
    expenses.append(expense)
    save_expenses(expenses)
    print("Expense saved.")


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

    print("\nExpenses")
    for expense in expenses:
        print(
            "- {} | {} | ${:.2f}".format(
                expense["description"],
                expense["category"],
                expense["amount"],
            )
        )

    total = sum(expense["amount"] for expense in expenses)
    print("Total: ${:.2f}".format(total))


def main():
    expenses = load_expenses()

    while True:
        print("\nBudget Buddy")
        print("1. Add expense")
        print("2. List expenses")
        print("3. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            list_expenses(expenses)
        elif choice == "3":
            print("Goodbye!")
            break
        else:
            print("Choose 1, 2, or 3.")


if __name__ == "__main__":
    main()

How to Use This Reference

This is the cumulative app for this step. Its startup path loads the saved list before the menu begins.

Restart Budget Buddy and prove persistence

A saved file proves that data reached disk. Persistence is confirmed when a fresh process loads that data back into memory.

Before you restart Budget Buddy, do you think option 2 will rebuild the previous row or show the empty-state message?

  • Enter 3 at the menu prompt to quit.
  • Select the Run Python File button in the editor toolbar.
  • Enter 2 at the menu prompt.

You will see the same expense row ending in $4.25. You will also see Total: $4.25.

That is the persistence loop working. Budget Buddy now rebuilds its expenses list from disk whenever it starts.

Your expenses can now outlive the running program. Next, you will protect the saved list from mistyped amounts.

Protect the App from Invalid Amounts

Budget Buddy now restores expenses from JSON after each restart. That persistence only helps when the app survives long enough to save clean data.

A mistyped amount currently has no safe path through float(). You will use exception handling to recover from nonnumeric input. You will also block zero and negative amounts.

In this step, get ready to:
  • Catch nonnumeric amounts without ending the menu.
  • Reject zero and negative amounts before saving.
  • Restart the app to prove that only valid expenses persist.
Catch a nonnumeric amount

The current conversion expects text that represents a number. Letters such as abc cause float() to raise a ValueError.

  • Select the Run Python File button in the top-right corner of the open editor.
  • Enter 1 at the menu prompt.
  • Enter Lunch at the description prompt.
  • Enter Food at the category prompt.

Before you enter the amount, consider whether Budget Buddy returns to its menu or ends the session.

  • Enter abc at the amount prompt.

You will see a traceback that ends with ValueError. Budget Buddy exits instead of returning to the menu.

That failure is expected in the current version. It proves that the amount conversion needs a recovery path.

  • Return to the open expense_tracker.py file.
  • Find amount = float(input("Amount: $").strip()) inside add_expense().
  • Replace that line with this guarded conversion:
    try:
        amount = float(input("Amount: $").strip())
    except ValueError:
        print("Please enter a number, such as 12.50.")
        return

What does this code do?

  • The try block attempts to convert the entered text into a number.
  • The except ValueError block catches text that cannot become a number.
  • The return statement exits add_expense() before the invalid expense reaches the list.

Before you test the change, consider whether the same input now returns you to the menu.

  • Save expense_tracker.py.
  • Select the Run Python File button.
  • Enter 1 at the menu prompt.
  • Enter Lunch at the description prompt.
  • Enter Food at the category prompt.
  • Enter abc at the amount prompt.

You should see Please enter a number, such as 12.50. followed by the Budget Buddy menu. That weak spot is closed. Nonnumeric input no longer ends the session.

Still seeing a traceback?

  • Check that except ValueError: starts at the same indentation level as try:.
  • Check that the amount conversion is indented inside the try block.
  • Check that return is indented inside the except block.

Help me fix my ValueError handler.

Reject zero and negative amounts

A value such as -3 converts successfully because it is numeric. A separate condition must stop nonpositive values before the expense is created.

  • Find the except ValueError block inside add_expense().
  • Place your cursor on the blank line immediately below that block.
  • Add the nonpositive amount guard by pasting this code:
    if amount <= 0:
        print("Amount must be greater than zero.")
        return

How does the guard work?

  • The condition catches zero and every negative amount.
  • The message explains the valid range to the person using the app.
  • The return statement prevents expenses.append(expense) from running.

Before you test the new guard, consider whether -3 reaches the saved expense list.

  • Save expense_tracker.py.
  • Select the Run Python File button.
  • Enter 1 at the menu prompt.
  • Enter Refund at the description prompt.
  • Enter Other at the category prompt.
  • Enter -3 at the amount prompt.

You should see Amount must be greater than zero. followed by the menu. The second guard is working. Negative amounts no longer reach the saved data.

Was the negative amount saved?

  • Check that the new if amount <= 0: block appears before expense = {.
  • Check that return is indented inside the new if block.

Help me stop nonpositive amounts from being saved.

Prove only valid expenses persist

Both guards return before Budget Buddy creates or saves an expense dictionary. A restart proves that rejected input never entered expenses.json.

  • Enter 1 at the menu prompt.
  • Enter Rejected expense at the description prompt.
  • Enter Test at the category prompt.
  • Enter abc at the amount prompt.

You should see Please enter a number, such as 12.50. followed by the menu. The rejected expense remains outside the saved list.

  • Enter 1 at the menu prompt.
  • Enter Notebook at the description prompt.
  • Enter Supplies at the category prompt.
  • Enter 4.25 at the amount prompt.

You should see Expense saved. followed by the menu. This valid expense has reached both the in-memory list and expenses.json.

  • Enter 3 at the menu prompt to quit Budget Buddy.

Before you restart the app, consider which of the two test entries you expect to find.

  • Select the Run Python File button.
  • Enter 2 at the menu prompt.

You should see Notebook | Supplies | $4.25 in the expense list. You should not see Rejected expense.

✔️ Awesome, I've got everything!

Your saved file now matches the validated version of Budget Buddy.

ⓧ I'd like to double check the full code

This is the complete expense_tracker.py file after adding both amount guards.

import json

DATA_FILE = "expenses.json"


def load_expenses():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []


def save_expenses(expenses):
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(expenses, file, indent=2)


def add_expense(expenses):
    description = input("Description: ").strip()
    category = input("Category: ").strip()

    try:
        amount = float(input("Amount: $").strip())
    except ValueError:
        print("Please enter a number, such as 12.50.")
        return

    if amount <= 0:
        print("Amount must be greater than zero.")
        return

    expense = {
        "description": description,
        "category": category,
        "amount": amount,
    }
    expenses.append(expense)
    save_expenses(expenses)
    print("Expense saved.")


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

    print("\nExpenses")
    for expense in expenses:
        print(
            "- {} | {} | ${:.2f}".format(
                expense["description"],
                expense["category"],
                expense["amount"],
            )
        )

    total = sum(expense["amount"] for expense in expenses)
    print("Total: ${:.2f}".format(total))


def main():
    expenses = load_expenses()

    while True:
        print("\nBudget Buddy")
        print("1. Add expense")
        print("2. List expenses")
        print("3. Quit")

        choice = input("Choose an option: ").strip()

        if choice == "1":
            add_expense(expenses)
        elif choice == "2":
            list_expenses(expenses)
        elif choice == "3":
            print("Goodbye!")
            break
        else:
            print("Choose 1, 2, or 3.")


if __name__ == "__main__":
    main()

What should match?

  • The try and except ValueError block surrounds only the amount conversion.
  • The nonpositive amount guard appears before the expense dictionary.

You have turned two bad inputs into safe menu responses. Budget Buddy now persists only valid expenses.

Secret mission

Filter Expenses by Category

Your saved expense list can answer focused category questions. In this mission, you will add a case-insensitive filter that shows matching expenses. It also calculates their subtotal.

Clean Up Your Resources

Clean Up Your Resources

Budget Buddy runs entirely on your local machine, so there are no ongoing costs. Decide whether to keep your files, pause your work, or delete the project entirely.

Resources you used:

  • The local budget-buddy folder.
  • The Python application in expense_tracker.py.
  • The saved expense records in expenses.json.

Keep everything running

No action is needed. Choose this if you want to keep using Budget Buddy or extend it with more features.

  • Keep expense_tracker.py in the budget-buddy folder.
  • Keep expenses.json in the budget-buddy folder.
  • Return to the workspace whenever you want to run Budget Buddy again.

Pause - I'll come back to this later

Pausing closes the app plus editor while preserving your saved expenses. No background service continues running after the app closes.

  • Enter 4 at the Budget Buddy menu if the app is running.
  • Close Visual Studio Code.
  • Leave the budget-buddy folder on your computer.
  • Return to the same folder when you are ready to continue.

Delete - I don't want to use this again

Deleting the budget-buddy folder permanently removes your code plus saved expense history. This clears every local resource created during the project.

  • Enter 4 at the Budget Buddy menu if the app is running.
  • Close Visual Studio Code.
  • Press the Windows key to open search.
  • Type File Explorer into the search box.
  • Press Enter to open File Explorer.
  • Navigate to the location where you created the budget-buddy folder.
  • Select the budget-buddy folder.
  • Press Shift+Delete to remove the folder permanently.
  • Confirm the deletion when Windows asks.
  • Search the same location for budget-buddy to confirm the folder is gone.

Nice Work!

Nice Work!

You did it! Budget Buddy is now a persistent Python terminal app that records expenses safely between runs.

You've learned how to:

  • Build an interactive Python terminal app that turns descriptions, categories, and amounts into structured expense dictionaries.
  • Manage multiple expenses through a menu using functions, lists, loops, and conditionals. Calculate a running total whenever you list your expenses.
  • Replace temporary in-memory state with JSON persistence in expenses.json. Protect the saved data by rejecting nonnumeric or nonpositive amounts without crashing.
  • Secret Mission: Extend the menu with a case-insensitive category filter that displays matching expenses and their subtotal.

Ready to quiz yourself?