1 The Problem
We want an expense tracker that records each spend with an amount, category, and date, then reports: total spent, spending per category, and spending per month. It builds on summarising by adding dates and grouped reporting — closer to a real finance tool.
2 How to Think About It
Think about the reporting, before any code:
3 The Build — explained part by part
Here is the complete tracker. Each part is explained below.
import json
import os
from collections import defaultdict
FILE = "expenses.json"
def load():
if os.path.exists(FILE):
with open(FILE) as f:
return json.load(f)
return []
def save(expenses):
with open(FILE, "w") as f:
json.dump(expenses, f)
def total(expenses):
return sum(e["amount"] for e in expenses)
def by_category(expenses):
totals = defaultdict(float)
for e in expenses:
totals[e["category"]] += e["amount"]
return dict(totals)
def by_month(expenses):
totals = defaultdict(float)
for e in expenses:
month = e["date"][:7] # "2026-06-24" -> "2026-06"
totals[month] += e["amount"]
return dict(totals)
if __name__ == "__main__":
expenses = load()
expenses.append({"amount": 25.0, "category": "food", "date": "2026-06-24"})
save(expenses)
print("Total:", total(expenses))
print("By category:", by_category(expenses))
print("By month:", by_month(expenses))
"2026-06-24".total(expenses) — sum every amount in one line.
by_category — the same grouping you learned in the budget tracker: a
defaultdict totals each category.e["date"][:7] — the new trick: take the first 7 characters of the date (
"2026-06-24" → "2026-06") to get the year-month. Grouping by that gives monthly totals.by_month — group amounts by that year-month string, so you see spending per month without any date library.
[:4] gives only the year, [:10] the whole day.[:7] to get year-month like 2026-06.YYYY-MM-DD so slicing is reliable.if category in totals — unnecessary with defaultdict.defaultdict(float) so keys start at 0 automatically.4 Test & Prove Each Part
We test the three reports — total, by category, by month — with known expenses.
from collections import defaultdict
def total(expenses):
return sum(e["amount"] for e in expenses)
def by_category(expenses):
totals = defaultdict(float)
for e in expenses:
totals[e["category"]] += e["amount"]
return dict(totals)
def by_month(expenses):
totals = defaultdict(float)
for e in expenses:
totals[e["date"][:7]] += e["amount"]
return dict(totals)
EXPENSES = [
{"amount": 10, "category": "food", "date": "2026-06-01"},
{"amount": 20, "category": "food", "date": "2026-07-01"},
{"amount": 5, "category": "travel", "date": "2026-06-15"},
]
def test_total():
"""Total is 35."""
assert total(EXPENSES) == 35
def test_by_category():
"""Food totals 30, travel 5."""
result = by_category(EXPENSES)
assert result["food"] == 30
assert result["travel"] == 5
def test_by_month():
"""June totals 15, July 20."""
result = by_month(EXPENSES)
assert result["2026-06"] == 15
assert result["2026-07"] == 20
Run with pytest -v. Each report is a small function tested with a known set of expenses. The month test proves the date-slicing trick groups correctly.
5 The Interface
What it expects
{"amount": 25, "category": "food", "date": "2026-06-24"}What it returns
Total: 25.0
By category: {'food': 25.0}
By month: {'2026-06': 25.0}6 Run It & Automate It
python3 expenses.pyLog expenses and view totals by category and month. Saved in
expenses.json.Jenkins runs the tests automatically — each line explained below.
Total: 25.0
By category: {'food': 25.0}
By month: {'2026-06': 25.0}YYYY-MM-DD and you slice [:7].KeyError in a report.defaultdict(float) instead of a plain dict.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Budget alerts. Warn when a category exceeds a monthly limit. (Teaches: comparing totals to limits.)
- Date range filter. Report only expenses between two dates. (Teaches: filtering by date.)
- Use real dates. Switch to the
datetimemodule for proper date maths. (Teaches: date handling.)