Tech With Galvan
Back to Articles
Development2/24/2025 • 3 min read

Expense Tracker using Python and Streamlit

Track spending with Python and Streamlit — log expenses by category, see monthly totals, and visualize where your money goes.

Galvan
Galvan

Founder & Creator

Introduction

An expense tracker is the project where Python stops being a hobby and starts saving you money. In about 120 lines you get a form for logging expenses, automatic monthly summaries, and charts that make overspending painfully obvious. It combines the state handling of the to-do list app with the data-crunching patterns from the Pandas dashboard.

The trick that keeps this app simple is Pandas as the storage engine: expenses live in a CSV file, so filtering by month, grouping by category, and summing totals are one-liners instead of hand-rolled loops.

Features

  • Log expenses — amount, category, and note via a form.
  • Category breakdown — pie chart of spending by category.
  • Monthly summary — total, average per day, and biggest expense.
  • History table — recent expenses, newest first.
  • Delete entries — remove mistakes with one click.

Prerequisites

  • Python 3.8+ — from python.org.
  • Dependencies — install with pip:
pip install streamlit pandas matplotlib

Step 1: Create the Script

Save this as expense_tracker.py:

import streamlit as st
import pandas as pd
from datetime import date
import os

CSV_FILE = "expenses.csv"
CATEGORIES = ["Food", "Travel", "Bills", "Shopping", "Fun", "Other"]


def load_expenses():
    if os.path.exists(CSV_FILE):
        return pd.read_csv(CSV_FILE, parse_dates=["date"])
    return pd.DataFrame(columns=["date", "amount", "category", "note"])


st.set_page_config(page_title="Expense Tracker", page_icon="💰", layout="centered")
st.title("💰 Expense Tracker")

expenses = load_expenses()

# --- Add expense form ---
with st.form("add_expense", clear_on_submit=True):
    col1, col2 = st.columns(2)
    amount = col1.number_input("Amount", min_value=0.01, format="%.2f")
    category = col2.selectbox("Category", CATEGORIES)
    note = st.text_input("Note (optional)", placeholder="Groceries, fuel, movie...")
    submitted = st.form_submit_button("Add Expense")
    if submitted:
        new_row = pd.DataFrame(
            [{"date": date.today(), "amount": amount, "category": category, "note": note}]
        )
        expenses = pd.concat([new_row, expenses], ignore_index=True)
        expenses.to_csv(CSV_FILE, index=False)
        st.success(f"Added {category} expense!")

# --- Summary ---
if not expenses.empty:
    expenses["month"] = expenses["date"].dt.to_period("M")
    this_month = expenses[expenses["month"] == pd.Period(date.today(), "M")]

    c1, c2, c3 = st.columns(3)
    c1.metric("This month", f"₹{this_month['amount'].sum():,.0f}")
    c2.metric("Daily average", f"₹{this_month['amount'].mean():,.0f}")
    c3.metric("Entries", len(this_month))

    left, right = st.columns(2)
    with left:
        by_cat = this_month.groupby("category")["amount"].sum()
        st.bar_chart(by_cat)
    with right:
        st.dataframe(
            this_month[["date", "amount", "category", "note"]]
            .sort_values("date", ascending=False),
            use_container_width=True,
            hide_index=True,
        )
else:
    st.info("No expenses logged yet.")

Step 2: Run the App

streamlit run expense_tracker.py

Log a few expenses across categories, then watch the bar chart and metrics update instantly.

How It Works

Everything rides on two Pandas idioms. First, df["date"].dt.to_period("M") converts timestamps into month periods, so filtering “this month” is a simple equality check — no manual date-range math. Second, groupby("category")["amount"].sum() produces the aggregated series that st.bar_chart renders directly; Pandas indexes become chart labels for free.

The form uses st.form, which batches its widgets into a single rerun. That matters here: without a form, every keystroke in the amount box would trigger a full script rerun. Forms are the standard answer to “my Streamlit app reruns too much”.

Storage stays boring on purpose — a CSV round-tripped with to_csv/read_csv. It has zero setup, opens in Excel, and is trivially portable to the home server you might host it on later.

Common Errors & Fixes

  • KeyError: 'date' on first run — read_csv on a missing file returns an empty frame without columns; the code guards with os.path.exists, so check the file path if you renamed it.
  • Charts show nothing — you grouped by a column with all NaN amounts; make sure number_input has min_value=0.01 so amounts are always real.
  • Duplicate rows after refresh — the insert ran outside the form guard; keep the to_csv write inside if submitted:.
  • Dates parsed as strings — pass parse_dates=["date"] to read_csv, otherwise .dt accessors fail.

Key Concepts

  • st.form — batches inputs into one rerun; essential for entry forms.
  • Period filtering — to_period("M") makes month comparisons trivial.
  • groupby + chart — aggregated Series render as labeled charts directly.
  • CSV as a database — fine for single-user, file-sized datasets.

What to Try Next

  • Add a monthly budget input and color the total metric green/red against it.
  • Export a monthly report with to_csv through st.download_button.
  • Add a trend line of total spend per month using st.line_chart on a grouped series.
  • Support multiple users with a name column and a sidebar filter.

FAQ

Why CSV instead of SQLite?

For one user and a few thousand rows, CSV is simpler, human-readable, and opens in Excel. Switch to SQLite when you need concurrent writes or faster queries on large data.

Can I track income too?

Yes — add a type column (“income”/“expense”) with a radio in the form, and compute net savings as income minus expenses per month.

How do I back up my data?

Copy expenses.csv — that’s the entire database. For automation, a nightly copy script on your home server works well.