Editor

Expense Tracker in Python

Intermediate · Useful tools · about 60 minutes to build · 139 lines

Record what you spend with a date, a category and a note, then see where the money went in a table of totals by category.

The program

Press Run and answer its questions in the box that appears under the code.

expense_tracker.py
"""An expense tracker that saves every expense to a CSV file."""
import csv
import math
import os
from datetime import date
FILE_NAME = "expenses.csv"
FIELDS = ["date", "category", "amount", "note"]
CATEGORIES = ["Food", "Transport", "Bills", "Shopping", "Fun", "Other"]
BAR_WIDTH = 20
# Saving and loading
def load_expenses(file_name):
"""Read saved expenses, or return an empty list if there is no file yet."""
expenses = []
try:
with open(file_name, newline="", encoding="utf-8") as file:
for row in csv.DictReader(file):
try:
row["amount"] = float(row["amount"])
except (KeyError, TypeError, ValueError):
continue # Skip a damaged line instead of crashing
row["note"] = row.get("note") or ""
expenses.append(row)
except FileNotFoundError:
pass # First run: nothing has been saved yet
return expenses
def save_expense(file_name, expense):
"""Add one expense to the end of the file, with a header row if it is new."""
is_new_file = not os.path.exists(file_name)
with open(file_name, "a", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
if is_new_file:
writer.writerow(FIELDS)
writer.writerow([expense["date"], expense["category"],
f"{expense['amount']:.2f}", expense["note"]])
# Asking for one expense
def read_date():
"""Ask for a date in YYYY-MM-DD form; Enter means today."""
while True:
text = input("Date (YYYY-MM-DD, Enter for today): ").strip()
if text == "":
return date.today().isoformat()
try:
return date.fromisoformat(text).isoformat()
except ValueError:
print(" Please type the date like 2026-10-05.")
def choose_category():
"""Show the numbered categories and return the one picked."""
print(" " + " ".join(f"{n}) {name}" for n, name in enumerate(CATEGORIES, 1)))
while True:
text = input(f"Category 1-{len(CATEGORIES)}: ").strip()
if text.isdecimal() and 1 <= int(text) <= len(CATEGORIES):
return CATEGORIES[int(text) - 1]
print(f" Please type a number from 1 to {len(CATEGORIES)}.")
def read_amount():
"""Ask until the user types an amount above zero."""
while True:
try:
amount = round(float(input("Amount: ")), 2)
if amount > 0 and math.isfinite(amount):
return amount
except ValueError:
pass
print(" Please type an amount above zero, like 120 or 45.50.")
# Reports
def show_expenses(expenses):
"""Print every expense as a table."""
print(f" {'Date':<12}{'Category':<11}{'Amount':>9} Note")
for expense in expenses:
line = (f" {expense['date']:<12}{expense['category']:<11}"
f"{expense['amount']:>9.2f} {expense['note']}")
print(line.rstrip())
def show_totals(expenses):
"""Print the total for each category with its share of all spending."""
totals = {}
for expense in expenses:
category = expense["category"]
totals[category] = totals.get(category, 0) + expense["amount"]
grand_total = sum(totals.values())
print(f" {'Category':<11}{'Amount':>9} Share")
ranked = sorted(totals.items(), key=lambda item: item[1], reverse=True)
for category, total in ranked:
share = total / grand_total
bar = "#" * round(share * BAR_WIDTH)
print(f" {category:<11}{total:>9.2f} {share:>4.0%} {bar}")
print(f" {'Total':<11}{grand_total:>9.2f}")
def main():
"""Load the expenses and run the menu until the user quits."""
expenses = load_expenses(FILE_NAME)
print(f"Expense tracker: {len(expenses)} expense(s) loaded from {FILE_NAME}")
while True:
print()
print("1) Add expense 2) List expenses 3) Totals by category 4) Quit")
choice = input("Choose 1-4: ").strip()
if choice == "1":
expense = {
"date": read_date(),
"category": choose_category(),
"amount": read_amount(),
"note": input("Note (optional): ").strip(),
}
expenses.append(expense)
save_expense(FILE_NAME, expense)
print(f" Saved: {expense['amount']:.2f} on {expense['category']}")
elif choice in ("2", "3") and not expenses:
print(" No expenses yet. Choose 1 to add one.")
elif choice == "2":
show_expenses(expenses)
elif choice == "3":
show_totals(expenses)
elif choice == "4":
print(f"Your expenses are saved in {FILE_NAME}. Goodbye.")
break
else:
print(" Please choose a number from 1 to 4.")
if __name__ == "__main__":
main()

Open it in the full editor to save it, share it and keep building.

Sample run

What the program printed, and what was typed (in yellow).

Expense tracker: 0 expense(s) loaded from expenses.csv

1) Add expense  2) List expenses  3) Totals by category  4) Quit
Choose 1-4: 1
Date (YYYY-MM-DD, Enter for today): 2026-10-01
  1) Food  2) Transport  3) Bills  4) Shopping  5) Fun  6) Other
Category 1-6: 1
Amount: 250
Note (optional): Groceries
  Saved: 250.00 on Food

1) Add expense  2) List expenses  3) Totals by category  4) Quit
Choose 1-4: 1
Date (YYYY-MM-DD, Enter for today): 2026-10-03
  1) Food  2) Transport  3) Bills  4) Shopping  5) Fun  6) Other
Category 1-6: 2
Amount: 120.50
Note (optional): Bus pass
  Saved: 120.50 on Transport

1) Add expense  2) List expenses  3) Totals by category  4) Quit
Choose 1-4: 1
Date (YYYY-MM-DD, Enter for today): 3 Oct
  Please type the date like 2026-10-05.
Date (YYYY-MM-DD, Enter for today): 2026-10-05
  1) Food  2) Transport  3) Bills  4) Shopping  5) Fun  6) Other
Category 1-6: 1
Amount: 80
Note (optional): 
  Saved: 80.00 on Food

1) Add expense  2) List expenses  3) Totals by category  4) Quit
Choose 1-4: 1
Date (YYYY-MM-DD, Enter for today): 2026-10-06
  1) Food  2) Transport  3) Bills  4) Shopping  5) Fun  6) Other
Category 1-6: 3
Amount: 499
Note (optional): Phone bill
  Saved: 499.00 on Bills

1) Add expense  2) List expenses  3) Totals by category  4) Quit
Choose 1-4: 2
  Date        Category      Amount  Note
  2026-10-01  Food          250.00  Groceries
  2026-10-03  Transport     120.50  Bus pass
  2026-10-05  Food           80.00
  2026-10-06  Bills         499.00  Phone bill

1) Add expense  2) List expenses  3) Totals by category  4) Quit
Choose 1-4: 3
  Category      Amount  Share
  Bills         499.00   53%  ###########
  Food          330.00   35%  #######
  Transport     120.50   13%  ###
  Total         949.50

1) Add expense  2) List expenses  3) Totals by category  4) Quit
Choose 1-4: 4
Your expenses are saved in expenses.csv. Goodbye.

How it is built

1. Reading the CSV file

csv.DictReader turns each row into a dictionary keyed by the header row. A missing file means a first run, and a row whose amount is not a number is skipped instead of stopping the program.

def load_expenses(file_name):
    """Read saved expenses, or return an empty list if there is no file yet."""
    expenses = []
    try:
        with open(file_name, newline="", encoding="utf-8") as file:
            for row in csv.DictReader(file):
                try:
                    row["amount"] = float(row["amount"])
                except (KeyError, TypeError, ValueError):
                    continue  # Skip a damaged line instead of crashing
                row["note"] = row.get("note") or ""
                expenses.append(row)
    except FileNotFoundError:
        pass  # First run: nothing has been saved yet
    return expenses

2. Adding one row

Opening with "a" adds to the end of the file. The header row is written only when the file did not exist yet, which os.path.exists checks before opening it.

def save_expense(file_name, expense):
    """Add one expense to the end of the file, with a header row if it is new."""
    is_new_file = not os.path.exists(file_name)
    with open(file_name, "a", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        if is_new_file:
            writer.writerow(FIELDS)
        writer.writerow([expense["date"], expense["category"],
                         f"{expense['amount']:.2f}", expense["note"]])

3. Checking the date

date.fromisoformat raises ValueError for text like 3 Oct, so the loop asks again. Pressing Enter uses date.today().

def read_date():
    """Ask for a date in YYYY-MM-DD form; Enter means today."""
    while True:
        text = input("Date (YYYY-MM-DD, Enter for today): ").strip()
        if text == "":
            return date.today().isoformat()
        try:
            return date.fromisoformat(text).isoformat()
        except ValueError:
            print("  Please type the date like 2026-10-05.")

4. Totals by category

A dictionary keeps one running total per category with totals.get(category, 0). Sorting by the total puts the biggest spending first, and each # in a bar stands for 5 percent.

def show_totals(expenses):
    """Print the total for each category with its share of all spending."""
    totals = {}
    for expense in expenses:
        category = expense["category"]
        totals[category] = totals.get(category, 0) + expense["amount"]
    grand_total = sum(totals.values())
    print(f"  {'Category':<11}{'Amount':>9}  Share")
    ranked = sorted(totals.items(), key=lambda item: item[1], reverse=True)
    for category, total in ranked:
        share = total / grand_total
        bar = "#" * round(share * BAR_WIDTH)
        print(f"  {category:<11}{total:>9.2f}  {share:>4.0%}  {bar}")
    print(f"  {'Total':<11}{grand_total:>9.2f}")

5. The menu

A new expense goes into the list in memory and into the file at once, so the two always agree. Listing and totals wait until there is at least one expense.

def main():
    """Load the expenses and run the menu until the user quits."""
    expenses = load_expenses(FILE_NAME)
    print(f"Expense tracker: {len(expenses)} expense(s) loaded from {FILE_NAME}")
    while True:
        print()
        print("1) Add expense  2) List expenses  3) Totals by category  4) Quit")
        choice = input("Choose 1-4: ").strip()
        if choice == "1":
            expense = {
                "date": read_date(),
                "category": choose_category(),
                "amount": read_amount(),
                "note": input("Note (optional): ").strip(),
            }
            expenses.append(expense)
            save_expense(FILE_NAME, expense)
            print(f"  Saved: {expense['amount']:.2f} on {expense['category']}")
        elif choice in ("2", "3") and not expenses:
            print("  No expenses yet. Choose 1 to add one.")
        elif choice == "2":
            show_expenses(expenses)
        elif choice == "3":
            show_totals(expenses)
        elif choice == "4":
            print(f"Your expenses are saved in {FILE_NAME}. Goodbye.")
            break
        else:
            print("  Please choose a number from 1 to 4.")

Make it bigger

  • Add a month filter, such as 2026-10, to the list and the totals.
  • Set a monthly budget per category and warn when spending passes it.
  • Let the user delete a wrong entry, then write the whole CSV file again.
  • Open expenses.csv in a spreadsheet and make a chart from the same data.

What it practises