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.
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.csvin a spreadsheet and make a chart from the same data.