Python Expense Tracker
Table of Contents + −
In the last lesson, Password Generator, you built a handy tool. But it forgot everything the moment it closed. Real apps remember things. This project fixes that. We are building an expense tracker that records what you spend and saves it to a file, so your data is still there next time you open it. You will combine dictionaries, lists, and file handling into one program, the exact shape of a real app that stores data.
🤔 Why save data to a file?
Every program so far lost its data when it closed. Variables live in memory, and memory is wiped when the program ends. That is fine for a calculator, but useless for tracking expenses. You want last week’s spending to still be there today.
To remember things between runs, a program must:
- Write its data to a file on the disk before it closes.
- Read that file back when it starts again.
We will store each expense as a small dictionary, keep all of them in a list, and save that list to a file using JSON, a simple text format for data that you met in the file handling module. JSON is perfect here because it turns a list of dictionaries into text and back with almost no effort.
🧱 Step 1: The shape of our data
Before writing code, let’s decide how an expense looks. Each one has a description (what you bought) and an amount (how much). A dictionary fits this perfectly, and a list holds all of them.
This is the data shape we will use:
# One expense is a dictionaryexpense = {"item": "Coffee", "amount": 4.5}
# All expenses live in a listexpenses = [ {"item": "Coffee", "amount": 4.5}, {"item": "Bus ticket", "amount": 2.0},]So each dictionary holds one purchase with two clear keys. The list expenses is the whole record. This pairing, a list of dictionaries, is one of the most common ways to hold data in real programs, so it is worth getting comfortable with.
💾 Step 2: Saving and loading with JSON
Now the part that gives the app a memory. The json module turns our list into text we can write to a file, and reads it back into a list when we start again.
These two functions handle saving and loading:
import json
def save_expenses(expenses): with open("expenses.json", "w") as f: json.dump(expenses, f)
def load_expenses(): try: with open("expenses.json", "r") as f: return json.load(f) except FileNotFoundError: return []Let’s read them:
save_expensesopens the file in write mode andjson.dumpwrites the whole list into it as text.load_expensesopens the file andjson.loadreads it back into a Python list.- The
try/exceptmatters. The very first time you run the app, the file does not exist yet, so we catchFileNotFoundErrorand return an empty list to start fresh.
So saving and loading are two short functions. The try / except is what stops the first run from crashing on a missing file.
➕ Step 3: Adding and showing expenses
Next we need to add a new expense and to show what we have. Adding means appending a dictionary to the list. Showing means looping through the list and printing each one, plus the total.
These two functions do that:
def add_expense(expenses, item, amount): expenses.append({"item": item, "amount": amount})
def show_expenses(expenses): if not expenses: print("No expenses yet.") return
total = 0 print("\nYour expenses:") for e in expenses: print(f"- {e['item']}: {e['amount']}") total += e["amount"] print(f"Total spent: {total}")What each one does:
add_expensebuilds a dictionary from the item and amount, then appends it to the list.show_expensesfirst checks if the list is empty and says so if it is.- Otherwise it loops through every expense, prints it, and keeps a running
totalby adding each amount.
🖥️ Step 4: The complete program
Here is the whole tracker, with a menu loop tying it together. It loads saved data at the start and saves again whenever you add something. Save it as tracker.py and run it.
import json
def save_expenses(expenses): with open("expenses.json", "w") as f: json.dump(expenses, f)
def load_expenses(): try: with open("expenses.json", "r") as f: return json.load(f) except FileNotFoundError: return []
def add_expense(expenses, item, amount): expenses.append({"item": item, "amount": amount})
def show_expenses(expenses): if not expenses: print("No expenses yet.") return total = 0 print("\nYour expenses:") for e in expenses: print(f"- {e['item']}: {e['amount']}") total += e["amount"] print(f"Total spent: {total}")
def main(): expenses = load_expenses() print("Expense Tracker")
while True: print("\n1) Add expense 2) Show expenses 3) Quit") choice = input("Choose an option: ")
if choice == "1": item = input("What did you buy? ") try: amount = float(input("How much? ")) except ValueError: print("That is not a number.") continue add_expense(expenses, item, amount) save_expenses(expenses) print("Saved!")
elif choice == "2": show_expenses(expenses)
elif choice == "3": print("Goodbye!") break
else: print("Pick 1, 2, or 3.")
main()Here is a sample run:
Output
Expense Tracker
1) Add expense 2) Show expenses 3) QuitChoose an option: 1What did you buy? CoffeeHow much? 4.5Saved!
1) Add expense 2) Show expenses 3) QuitChoose an option: 2
Your expenses:- Coffee: 4.5Total spent: 4.5
1) Add expense 2) Show expenses 3) QuitChoose an option: 3Goodbye!The real magic is what you cannot see in one run. Close the program and open it again, choose “Show expenses”, and the coffee is still there. That is because it was saved to expenses.json on the disk. Your app now has a memory.
⚠️ Common Mistakes
A few traps in this project:
- Forgetting to handle the missing file on the first run. Without the
try/exceptinload_expenses, the first start crashes withFileNotFoundError. The fallback empty list fixes it. - Forgetting to save after adding. If you only append to the list but never call
save_expenses, the new expense is gone when the program closes. Save after every change. - Not converting the amount to a number.
input()gives text, so the total would try to add strings. Usefloat()and catch bad input.
✅ Best Practices
Habits this project teaches:
- Store structured data as a list of dictionaries. It is clear and maps cleanly to JSON.
- Save right after every change, so a crash or close never loses recent data.
- Always handle the missing-file case when loading, so a fresh start works smoothly.
- Keep saving, loading, and showing in separate functions. Each does one job, which keeps the program easy to grow.
🧩 What You’ve Learned
✅ You built an expense tracker that remembers your data between runs by saving it to a file.
✅ Each expense is a dictionary, and all of them live in a list, a common real-world data shape.
✅ json.dump writes the list to a file as text, and json.load reads it back into a list.
✅ Catching FileNotFoundError lets the very first run start with an empty list instead of crashing.
✅ Saving after every change is what gives the app a reliable memory.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Why does the expense tracker save its data to a file?
Why: Variables live in memory, which is wiped when the program closes. Saving to a file lets the data survive between runs, giving the app a memory.
- 2
How is each expense stored in this project?
Why: Each expense is a dictionary like {'item': 'Coffee', 'amount': 4.5}, and they all live in one list, a list of dictionaries.
- 3
Why does load_expenses catch FileNotFoundError?
Why: The first time the app runs, expenses.json has not been created. Catching the error lets the program start fresh with an empty list.
- 4
What does json.dump(expenses, f) do?
Why: json.dump takes the Python list and writes it into the open file as JSON text, so it can be loaded back later with json.load.
🚀 What’s Next?
You built an app with a real memory using files and JSON. Next we scale that idea up into a fuller program: a student management system that adds, views, updates, and removes records.