Expense Tracker Project
Projects Beginner Projects Project

Python Expense Tracker Project

1Expense Tracker Project: overview

An expense tracker stores each purchase as a dictionary inside a list, adds new entries, totals the spending per category and saves everything to a file so the data survives between runs.

GoalBy the end you can model records as a list of dictionaries, write functions to add and summarise expenses, save and load them with JSON, and print a tidy text report.

Before this lesson: Python Dictionaries, Python Functions, Python File Handling, Working with JSON in Python

2Expense Tracker Project: diagram

expenses (a list of dicts)expenses (a list of dicts)datecategoryamount02024-05-01food12.5012024-05-02transport3.2022024-05-02food8.7532024-05-03fun15.00each row is one dict; totals["food"] = 12.50 + 8.75 = 21.25
Think of the data as a table: every row is one expense dictionary, and the list keeps the rows in order. Grouping rows by the highlighted category column gives the totals for the report.

3Expense Tracker Project: explained step by step

This project pulls lists, dictionaries, functions and files together into a tool you could really use: a small program that records what you spend and shows where the money went.

Step 1: Write down the requirements

Decide what the program must do before thinking about code:

  • record an expense with a date, a category and an amount;
  • show the total per category, biggest first, plus a grand total;
  • remember expenses between runs by saving them to a file;
  • refuse obviously wrong data, such as a negative amount.

Step 2: Choose the data structure

Every expense has the same three named fields — a job for a dictionary. Many expenses kept in order — a job for a list. So the whole data set is a list of dictionaries:

Python
expenses = [
    {"date": "2024-05-01", "category": "food", "amount": 12.50},
    {"date": "2024-05-02", "category": "transport", "amount": 3.20},
]
for item in expenses:
    print(item["date"], item["category"], item["amount"])

Named keys make the code read like the problem (item["amount"] instead of item[2]), and this shape converts straight to JSON — and later to a pandas DataFrame.

Step 3: Split the work into functions

  • add_expense(expenses, date, category, amount) builds one dictionary and appends it.
  • totals_by_category(expenses) returns a dictionary such as {"food": 21.25}.
  • Saving and loading write the list to a file and read it back.
  • The report prints the totals in neat columns.

Each function has one job, so you can test it on its own and improve it later without breaking the others.

Step 4: Running totals with a dictionary

To add up spending per category, keep a dictionary of running totals. totals.get(category, 0) returns the total so far, or 0 the first time a category appears, and you add the new amount to it.

Step 5: Save and load with JSON

json.dump(data, file) writes the list as text, and json.load(file) rebuilds it with the same keys, strings and numbers. When the program starts, it loads the file instead of starting from an empty list.

Python
import json

expenses = [{"date": "2024-05-01", "category": "food", "amount": 12.5}]
with open("expenses.json", "w") as file:
    json.dump(expenses, file)
with open("expenses.json") as file:
    print(json.load(file))

On this site, files live in the browser's temporary file system and vanish when you reload the page. On your own computer, the file is created in the folder you run the script from.

Step 6: Format the report

f-strings can pad and align values: f"{name:<10}" left-aligns a name in 10 characters and f"{amount:>7.2f}" right-aligns a number with two decimals, so the columns line up.

4Expense Tracker Project: syntax

expenses = []                                   # list of dicts
expenses.append({"date": d, "category": c, "amount": a})

totals = {}
for row in expenses:                            # running totals
    totals[row["category"]] = totals.get(row["category"], 0) + row["amount"]

with open("expenses.json", "w") as file:        # save
    json.dump(expenses, file)
with open("expenses.json") as file:             # load
    expenses = json.load(file)

print(f"{category:<10}{amount:>7.2f}")          # aligned report line

5Expense Tracker Project: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
import json

def add_expense(expenses, date, category, amount):
    expenses.append({"date": date, "category": category, "amount": amount})

def totals_by_category(expenses):
    totals = {}
    for row in expenses:
        totals[row["category"]] = totals.get(row["category"], 0) + row["amount"]
    return totals

expenses = []
add_expense(expenses, "2024-05-01", "food", 12.50)
add_expense(expenses, "2024-05-02", "transport", 3.20)
add_expense(expenses, "2024-05-02", "food", 8.75)
add_expense(expenses, "2024-05-03", "fun", 15.00)

with open("expenses.json", "w") as file:
    json.dump(expenses, file)
with open("expenses.json") as file:
    saved = json.load(file)
totals = totals_by_category(saved)
for category in sorted(totals, key=totals.get, reverse=True):
    print(f"{category:<10}{totals[category]:>7.2f}")
print(f"{'TOTAL':<10}{sum(totals.values()):>7.2f}")
Expected outputfood        21.25
fun         15.00
transport    3.20
TOTAL       39.45

7Expense Tracker Project: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 1The json module saves Python lists and dictionaries as text and reads them back.
  2. Line 3-4add_expense() builds a dictionary for one expense and appends it to the list it is given. Lists are changed in place, so nothing needs to be returned.
  3. Line 6-10totals_by_category() starts with an empty dictionary. For each expense, totals.get(category, 0) fetches the running total (0 for a new category) and the amount is added to it.
  4. Line 12-16Start with an empty list and record four expenses. Each one becomes a dictionary like {"date": "2024-05-01", "category": "food", "amount": 12.5}.
  5. Line 18-19Open the file in write mode ("w") and json.dump() writes the whole list as JSON text, replacing any older contents.
  6. Line 20-21Open it again for reading and json.load() rebuilds the list of dictionaries — exactly what the program would do the next time it starts.
  7. Line 22Calculate the totals from the loaded data: food 21.25, transport 3.2 and fun 15.0.
  8. Line 23-24sorted(totals, key=totals.get, reverse=True) orders the category names by their totals, biggest first. :<10 pads the name to 10 characters and :>7.2f right-aligns the amount with two decimals.
  9. Line 25The grand total is the sum of all category totals, printed in the same column layout.

8Expense Tracker Project: common mistakes

Saving with append mode instead of write mode

✗ Wrong

import json
expenses = [{"category": "food", "amount": 4.5}]
for run in range(2):          # the program saves twice
    with open("expenses.json", "a") as file:
        json.dump(expenses, file)
with open("expenses.json") as file:
    data = json.load(file)
json.decoder.JSONDecodeError: Extra data: line 1 column 38 (char 37)

✓ Right

import json
expenses = [{"category": "food", "amount": 4.5}]
for run in range(2):
    with open("expenses.json", "w") as file:
        json.dump(expenses, file)
with open("expenses.json") as file:
    data = json.load(file)
print(data)

Mode "a" adds a second JSON list after the first, and a JSON file may hold only one value. Each save writes the whole list, so open the file with "w" to replace the old contents.

Overwriting the total instead of adding to it

✗ Wrong

expenses = [{"category": "food", "amount": 12.5},
            {"category": "food", "amount": 8.75}]
totals = {}
for row in expenses:
    totals[row["category"]] = row["amount"]
print(totals)
Prints {'food': 8.75} — only the last food expense is counted

✓ Right

expenses = [{"category": "food", "amount": 12.5},
            {"category": "food", "amount": 8.75}]
totals = {}
for row in expenses:
    totals[row["category"]] = totals.get(row["category"], 0) + row["amount"]
print(totals)

Assigning replaces the old value. Read the current total with get(..., 0) and add the new amount to it.

Comparing the amount from input() as text

✗ Wrong

amount = input("Amount: ")
if amount <= 0:
    print("Amount must be positive")
TypeError: '<=' not supported between instances of 'str' and 'int'

✓ Right

amount = float(input("Amount: "))
if amount <= 0:
    print("Amount must be positive")

input() always returns a string, even when the user types digits. Convert it with float() before comparing or adding it up.

9Expense Tracker Project: exercises

Write your answer, then press Check. The checks run in your browser.

1. Predict the category totals

Predict the output Easy

Read the code and type exactly what it prints.

Read this code
expenses = [
    {"category": "food", "amount": 4.5},
    {"category": "books", "amount": 12.0},
    {"category": "food", "amount": 3.0},
]
totals = {}
for e in expenses:
    totals[e["category"]] = totals.get(e["category"], 0) + e["amount"]
print(totals)
print(len(expenses), "expenses")
print(f"food: {totals['food']:.2f}")

2. Validate new expenses

Write code Medium

Make add_expense() safer. It should:

  • raise ValueError if amount is 0 or negative;
  • raise ValueError if category is empty or only spaces;
  • otherwise store the category stripped and in lower case (so " Food " is saved as "food") and append the expense.

Invalid expenses must not be added to the list.

Your code
def add_expense(expenses, date, category, amount):
    expenses.append({"date": date, "category": category, "amount": amount})

expenses = []
add_expense(expenses, "2024-05-01", " Food ", 12.5)
print(expenses)

2 more exercises → 4 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

Why use a list of dictionaries instead of separate lists for dates, categories and amounts?

With separate lists, one expense is spread across three places and an insert or delete in one list can silently misalign the others. A dictionary keeps each expense's fields together under readable names, and the list keeps the expenses in order. The same shape also saves directly as JSON and loads into pandas.

Where is my expenses file saved when I run this in the browser?

In the page's temporary in-memory file system. It survives between runs on the same page but disappears when you reload or close it. Run the same code on your computer to get a real file in the folder you start Python from.

Should I store expenses in JSON or CSV?

JSON keeps numbers as numbers and handles nested data, so it loads back exactly as saved — a good default for a program's own data. CSV opens directly in a spreadsheet and is easy to share, but every value comes back as a string, so convert amounts with float() after loading.

Further reading

10Next lesson

You reached the endKeep practising with exercises

Related:
Working with JSON in PythonPython CSV FilesPandas BasicsPython Calculator Project

← Python Calculator Project · All Beginner Projects lessons · Practise in the IDE