AI Hints
Home›Python Tutorial›Three Real Projects
Python · Lesson 40 of 49

Project: Expense Tracker

CSV, grouping and reports.

⏱ 2 min read▶ Video at 2:01:32🎓 Free
Watch this lesson in the Python courseWatch on YouTube ↗Full Python course

Ava's second project is tracking her shop's expenses. The data arrives as a CSV file, with the date, the category, and the amount. She wants a monthly total for each category, the biggest expense, and a tidy report. This project uses csv, dictionaries, sorting, and formatted text.

Project: Expense Tracker
From the video: Project: Expense Tracker
  • Data arrives as a CSV file
  • Total each category, find the biggest
  • A tidy printed report

Reading the data

We read the CSV with DictReader. Each row becomes a dictionary. We convert the amount to a float, and the date to a real date object, so we can sort and filter by month. A small function, that returns a list of clean records, keeps the rest of the program simple.

exp_load.pypython
import csv
from datetime import date

def load(path):
    out = []
    with open(path) as f:
        for row in csv.DictReader(f):
            out.append({"date": date.fromisoformat(row["date"]), "cat": row["category"], "amt": float(row["amount"])})
    return out

records = load("expenses.csv")
print(len(records), records[0])
Output
6 {'date': datetime.date(2025, 1, 4), 'cat': 'soil', 'amt': 45.5}
Also uses expenses.csv
date,category,amount
2025-01-04,soil,45.50
2025-01-09,pots,120.00
2025-01-15,soil,30.25
2025-02-02,delivery,18.90
2025-02-11,pots,64.00
2025-02-20,soil,52.10

Totals by category

A dictionary is perfect for totals. For each record, add its amount to its category, using get with a default of zero. Then sort the categories, by amount, largest first. The max function with a key finds the single biggest expense. In just a few lines, the numbers answer Ava's questions.

exp_totals.pypython
records = [("soil", 45.5), ("pots", 120.0), ("soil", 30.25), ("delivery", 18.9), ("pots", 64.0), ("soil", 52.1)]
totals = {}
for cat, amt in records:
    totals[cat] = totals.get(cat, 0) + amt
ranked = sorted(totals.items(), key=lambda kv: kv[1], reverse=True)
print(ranked)
print(max(records, key=lambda r: r[1]))
Output
[('pots', 184.0), ('soil', 127.85), ('delivery', 18.9)]
('pots', 120.0)

A formatted report

Now a clean report. Format strings align the columns, with a width, and a precision. Left align the names, and right align the amounts. Add a rule line with repeated dashes, and a total at the bottom. Good output makes the data useful, to people, not only to programs.

exp_report.pypython
totals = {"pots": 184.0, "soil": 127.85, "delivery": 18.9}
print(f"{'Category':<12}{'Total':>10}")
print("-" * 22)
for cat, amt in sorted(totals.items(), key=lambda kv: -kv[1]):
    print(f"{cat:<12}{amt:>10.2f}")
print("-" * 22)
print(f"{'ALL':<12}{sum(totals.values()):>10.2f}")
Output
Category         Total
----------------------
pots            184.00
soil            127.85
delivery         18.90
----------------------
ALL             330.75

Grouping by month

Expenses by month need a key for each month. A defaultdict of floats creates a total automatically, the first time a key is used. The key is a year and month pair. Sorting the keys gives a timeline. This pattern, group then summarise, is the same one pandas uses on millions of rows.

exp_month.pypython
from collections import defaultdict
from datetime import date

rows = [(date(2025, 1, 4), 45.5), (date(2025, 1, 9), 120.0), (date(2025, 2, 2), 18.9), (date(2025, 2, 11), 64.0)]
by_month = defaultdict(float)
for d, amt in rows:
    by_month[(d.year, d.month)] += amt
for (y, m), total in sorted(by_month.items()):
    print(f"{y}-{m:02d}: {total:.2f}")
Output
2025-01: 165.50
2025-02: 82.90

Average per category

Your turn. From a list of category and amount pairs, compute the average amount for each category, and print them. Use a dictionary of lists. Ready? Here is one solution. Collect the amounts, then average each list.

✍️ Exercise: Average per category

Given (category, amount) pairs, print the average amount per category.

Show hint

setdefault(cat, []).append(amt)

Show solution
avg_cat.pypython
pairs = [("soil", 40), ("pots", 100), ("soil", 60), ("pots", 80)]
by_cat = {}
for cat, amt in pairs:
    by_cat.setdefault(cat, []).append(amt)
for cat, amts in by_cat.items():
    print(cat, sum(amts) / len(amts))
Output
soil 50.0
pots 90.0

Summary

Well done. You read a CSV, cleaned the data, totalled by category and by month, and printed a neat report. The same shape of program works for sales, scores, and almost any dataset. Next, a third project, with text, and classes, a word analyser.

Key takeaways
  • csv.DictReader to read rows
  • dict / defaultdict for totals
  • f-string widths for tidy tables
Next up: Project: Text Analyser — words, counts and a clean class