Project: Expense Tracker
CSV, grouping and reports.
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.

- 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.
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])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.10Totals 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.
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]))[('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.
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}")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.
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}")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.
Given (category, amount) pairs, print the average amount per category.
Show hint
setdefault(cat, []).append(amt)
Show solution
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))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