Intermediate · Projects: put it all together · topic 33 of 50 · 35 min
Project: expense tracker
Explanation
The plan
| Part | For our tracker |
|---|---|
| Inputs | Expenses: date (YYYY-MM-DD), category, amount in rupees; a monthly budget |
| Outputs | Totals per category, monthly total, biggest expense, budget message, trip settlement |
| Data shape | A list of dictionaries, one dictionary per expense |
Step 1: add an expense, with validation
Validation means checking data before you accept it. Bad data that gets in once causes wrong totals forever. We check the date with datetime, clean the category, and make sure the amount is a positive number. If anything is wrong we raise ValueError with a clear message.
from datetime import date
def add_expense(expenses, day, category, amount):
date.fromisoformat(day) # raises ValueError for a bad date
category = category.strip().lower()
if not category:
raise ValueError("Category cannot be empty")
if not isinstance(amount, (int, float)) or amount <= 0:
raise ValueError("Amount must be a positive number")
item = {"date": day, "category": category, "amount": amount}
expenses.append(item)
return item
expenses = []
add_expense(expenses, "2026-03-02", " Food ", 250)
add_expense(expenses, "2026-03-05", "Travel", 120)
try:
add_expense(expenses, "2026-03-07", "food", -50)
except ValueError as e:
print("Rejected:", e)
print(expenses)Step 2: totals
def total_by_category(expenses):
totals = {}
for e in expenses:
totals[e["category"]] = totals.get(e["category"], 0) + e["amount"]
return totals
def monthly_total(expenses, month):
# month looks like "2026-03"
return sum(e["amount"] for e in expenses if e["date"].startswith(month + "-"))
expenses = [
{"date": "2026-03-02", "category": "food", "amount": 250},
{"date": "2026-03-05", "category": "travel", "amount": 120},
{"date": "2026-03-18", "category": "food", "amount": 400},
{"date": "2026-04-01", "category": "rent", "amount": 6000},
]
print(total_by_category(expenses)) # {'food': 650, 'travel': 120, 'rent': 6000}
print(monthly_total(expenses, "2026-03")) # 770Step 3: biggest expense and budget warning
def biggest_expense(expenses):
if not expenses:
return None
return max(expenses, key=lambda e: e["amount"])
def budget_status(spent, budget):
if spent > budget:
return f"Over budget by Rs {spent - budget}"
used = spent / budget * 100
if used >= 80:
return f"Warning: {round(used)}% of budget used"
return "OK"
print(biggest_expense([{"date": "2026-03-02", "category": "food", "amount": 250},
{"date": "2026-03-09", "category": "phone", "amount": 799}]))
print(budget_status(8500, 10000)) # Warning: 85% of budget used
print(budget_status(10400, 10000)) # Over budget by Rs 400
print(budget_status(3000, 10000)) # OKStep 4: split a trip bill
Four friends go to Goa. Each person paid for different things. First find each person's fair share (total divided by people). Then the balance is paid minus share: a positive balance means the group owes you; a negative balance means you owe the group.
def balances(paid):
share = sum(paid.values()) / len(paid)
return {name: round(amount - share, 2) for name, amount in paid.items()}
def settle(paid):
bal = balances(paid)
owes = [[n, -b] for n, b in bal.items() if b < 0]
gets = [[n, b] for n, b in bal.items() if b > 0]
payments = []
i = j = 0
while i < len(owes) and j < len(gets):
amount = round(min(owes[i][1], gets[j][1]), 2)
payments.append((owes[i][0], gets[j][0], amount))
owes[i][1] -= amount
gets[j][1] -= amount
if owes[i][1] < 0.01:
i += 1
if gets[j][1] < 0.01:
j += 1
return payments
paid = {"Riya": 4000, "Arjun": 2000, "Sana": 1500, "Kabir": 500}
print(balances(paid)) # Riya +2000, Arjun 0, Sana -500, Kabir -1500
for who, to, amount in settle(paid):
print(f"{who} pays {to} Rs {amount}")This simple method gives a correct settlement with few payments. It is not always the absolute minimum number of payments, but it is what most bill-splitting apps do for small groups.
Step 5: test it
- An empty list: totals should be {} and the monthly total 0.
- A bad date like "2026-13-40" and a negative amount must raise ValueError.
- Budget exactly at 80 percent should warn; exactly at 100 percent is not over.
- In settle(), the sum of all balances should be (almost) zero.
Watch the lesson
Project: expense tracker · 1/7
In this project you will build the brain of an expense tracker app, the part that does the counting.
Prefer a video by a teacher? Find free videos on “python expense tracker project for beginners” on YouTube ↗
In real life: Hostel students sharing costs
Four college friends in a Pune hostel share groceries, a cook and the Wi-Fi bill. One person pays the Wi-Fi, another pays the cook, and the groceries are paid by whoever is at the shop. At month end, the balances function tells them exactly who should send money to whom on UPI, without arguments.
paid = {"Meera": 3200, "Tanvi": 1800, "Ishita": 2400, "Nidhi": 600}
share = sum(paid.values()) / len(paid)
for name, amount in paid.items():
diff = round(amount - share, 2)
print(name, "gets back" if diff > 0 else "pays", abs(diff))Practice (0/3 done)
Write total_by_category(expenses). Each expense is a dictionary with "date", "category" and "amount". Return a dictionary of category to total amount. An empty list gives {}.
Write monthly_total(expenses, month) where month looks like "2026-03". Return the sum of amounts whose date is in that month (dates look like "2026-03-18").
Write balances(paid) for a trip. paid is a dictionary of name to amount that person paid. Everyone should pay an equal share. Return a dictionary of name to (amount paid minus share), rounded to 2 decimals. Positive means the person gets money back.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.