Skip to content

Intermediate · Projects: put it all together · topic 33 of 50 · 35 min

Project: expense tracker

Explanation

You will build the logic of an expense tracker: add an expense safely, total by category, total for a month, find the biggest expense and warn when the budget is nearly used. At the end you will split a trip bill among friends and work out who owes whom.

The plan

PartFor our tracker
InputsExpenses: date (YYYY-MM-DD), category, amount in rupees; a monthly budget
OutputsTotals per category, monthly total, biggest expense, budget message, trip settlement
Data shapeA list of dictionaries, one dictionary per expense
Think of it like this: Think of the small diary your mother or a shopkeeper keeps: each line has a date, what the money was for and how much. Our list of dictionaries is that diary. Each function is one question you ask the diary: how much on food? how much in March?

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.

python
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)
Cleaning " Food " to "food" means Food, food and FOOD all count as one category.

Step 2: totals

python
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"))  # 770
Dates in YYYY-MM-DD form start with the year and month, so startswith works.
Tip: You could also use collections.defaultdict(int) or collections.Counter for totals. The dict.get(key, 0) version is shown because it works everywhere and is easy to read.

Step 3: biggest expense and budget warning

python
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))   # OK
An empty list has no biggest item, so we return None instead of letting max() crash.
Careful: Money and floats: 0.1 + 0.2 is not exactly 0.3. For a learning project, whole rupees (int) avoid the problem. Real payment apps store money in paise as integers, or use the decimal module.

Step 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.

python
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}")
Each person who owes pays the people who are owed, until everyone is at zero.

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

Project: expense tracker

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.

python
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))
Share is 2000 each; Meera gets back 1200, Nidhi pays 1400.

Practice (0/3 done)

Task 1 · Code · Python

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 {}.

Task 2 · Code · Python

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").

Task 3 · Code · Python

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

Question 1

1.Why do we clean the category with .strip().lower()?

Question 2

2.What should add_expense do when the amount is -50?

Question 3

3.What does biggest_expense([]) return in our project?

Question 4

4.Three friends paid 900, 300 and 0. What is the balance of the one who paid 900?

Question 5

5.Why do real payment apps often store money in paise as integers?

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.

← Previous: Project: marks and grade calculator