AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Project: Personal Expense Tracker

📚 Projects & Applied⏱️ 24 min read🎓 Grade 8
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 24 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open any UPI app on a parent's phone at the end of the month and you will see dozens of small payments — ₹15 for a bus ticket, ₹40 for a samosa, ₹199 for a mobile recharge, ₹550 for school stationery. Nobody remembers all of that from memory. The app remembers it because, underneath the colourful screen, it is doing something you already know how to do: storing a list of records and adding them up. In this project you will build a real, working Personal Expense Tracker in Python — a program that lets you add an expense, see all your expenses, and find out how much you spent on food versus transport versus recharge. Along the way you will discover exactly why professional software is built the way it is, because you will hit the same design problems that real programmers hit, in the same order.

This is not a "type this code and run it" exercise. Every version of the program below is broken in some specific, findable way, and you will see the break before you see the fix. That is the only way the design choices actually make sense instead of feeling arbitrary.

Step 0: Decide what the program must actually do

Before writing a single line of code, write down the requirements in plain words. A useful expense tracker needs to:

  • Let the user add a new expense (an amount, a category like Food or Transport, and a short note).
  • Show all expenses recorded so far.
  • Calculate the total money spent.
  • Break the total down by category, so the user can see where the money actually went.

Notice that none of these requirements mention "list" or "dictionary" or "loop" yet. Those are tools we will choose because they solve these requirements — not the other way around. This is the single biggest habit that separates a student who can only copy code from one who can actually build software: start from what the program must do, then pick the data structure that makes that easy.

Step 1: One expense is just a few variables

The smallest possible version of "record an expense" is three variables:

expense_amount = 50
expense_category = "Transport"
expense_note = "Bus fare to school"

print("You spent Rs.", expense_amount, "on", expense_category)

Run this and Python prints exactly one line:

You spent Rs. 50 on Transport

That works perfectly — for one expense. But a real day involves five or six expenses, not one. If you try to extend this approach by numbering your variables, watch what happens:

amount_1, category_1 = 50, "Transport"
amount_2, category_2 = 120, "Food"
amount_3, category_3 = 30, "Food"
amount_4, category_4 = 200, "Recharge"
amount_5, category_5 = 75, "Transport"

This already feels wrong, and your instinct is correct. To compute the total, you would have to write amount_1 + amount_2 + amount_3 + amount_4 + amount_5 by hand. If a sixth expense happens, you must rewrite the formula. Worse, the program has no way to ask "how many expenses are there?" — that number lives only in how many variable names you happened to type. A program that cannot count its own data cannot loop over it, and a program that cannot loop cannot scale from 5 expenses to 500. We need one container that can hold as many amounts as we throw at it, without us pre-deciding the count.

Step 2: A list removes the "how many" problem

A Python list is an ordered container that can grow. Instead of five separate amount variables, use one list of amounts:

amounts = [50, 120, 30, 200, 75]

total = 0
for amt in amounts:
    total = total + amt

print("Total spent: Rs.", total)

Trace this by hand, because tracing loops is the single most useful debugging skill you will ever build. total starts at 0. The loop visits each element of amounts in order and adds it in:

total = 0
total = 0 + 50   = 50
total = 50 + 120  = 170
total = 170 + 30  = 200
total = 200 + 200 = 400
total = 400 + 75  = 475

So the program prints Total spent: Rs. 475. Notice what the list bought us: len(amounts) tells the program how many expenses exist, and the for loop works whether there are 5 amounts or 5,000 — we never had to know the count in advance. That solves "how many," but we still have a second problem: each expense isn't just an amount, it's an amount and a category and a note.

Step 3: The parallel-list trap — a common and dangerous mistake

The instinctive next step is to make one list per field, keeping them "lined up" by position:

amounts = [50, 120, 30, 200, 75]
categories = ["Transport", "Food", "Food", "Recharge", "Transport"]
notes = ["Bus fare", "Canteen lunch", "Evening snack", "Mobile recharge", "Auto fare"]

This looks reasonable, and it will even work for the simple print-everything case. But it is a trap, and here is exactly how it breaks. Suppose you want to see your expenses from cheapest to costliest, so you sort the amounts:

amounts.sort()
print(amounts)

This prints [30, 50, 75, 120, 200]. The amounts list is now in a completely new order. But categories was never touched — it is still ["Transport", "Food", "Food", "Recharge", "Transport"]. Position 0 used to mean "the ₹50 bus fare," and now position 0 of amounts holds ₹30, while position 0 of categories still says "Transport." If you now print amounts[0] and categories[0] together, the program confidently tells you that you spent ₹30 on Transport — except that ₹30 was actually the evening snack. The bug does not crash the program or throw an error. It just silently produces a wrong answer, which is far more dangerous, because nothing warns you.

The misconception to fix here: many students believe that "using more lists" is a safe, simple way to store more information per item. It is not — it is only safe as long as you promise never to reorder, insert in the middle, or delete from any one list without doing the identical operation, in the identical position, to every other list. That promise is very easy to break by accident, and the resulting bug is very hard to notice, because Python gives no warning when lists silently go out of sync. What we actually want is for each expense's amount, category, and note to move around together, as one unit, no matter what.

Step 4: A dictionary bundles one expense's fields together

A Python dictionary stores a set of named fields as key–value pairs, all inside one object. Instead of scattering an expense's amount, category, and note across three separate lists, put them in one dictionary:

expense = {"amount": 50, "category": "Transport", "note": "Bus fare"}

print(expense["amount"])
print(expense["category"])

This prints 50 then Transport. The keys "amount", "category", and "note" are labels, and each one points to its own value inside the same object. Crucially, expense is now a single Python value — you can move it, copy it, or store it somewhere else, and its three fields travel together automatically, because they are not three separate variables anymore; they are three labelled compartments of one box.

Step 5: The real data structure — a list of dictionaries

Now combine the two ideas. We wanted a growable container (the list) holding items that each carry several fields (the dictionary). The natural structure is a list of dictionaries — one dictionary per expense, all held inside one list:

expenses = [
    {"amount": 50, "category": "Transport", "note": "Bus fare"},
    {"amount": 120, "category": "Food", "note": "Canteen lunch"},
    {"amount": 30, "category": "Food", "note": "Evening snack"},
    {"amount": 200, "category": "Recharge", "note": "Mobile recharge"},
    {"amount": 75, "category": "Transport", "note": "Auto fare"},
]

for e in expenses:
    print(e["category"], "- Rs.", e["amount"], "-", e["note"])

Tracing the loop: on each pass, e becomes the next dictionary in the list, and we read its three fields by name. This prints five lines:

Transport - Rs. 50 - Bus fare
Food - Rs. 120 - Canteen lunch
Food - Rs. 30 - Evening snack
Recharge - Rs. 200 - Mobile recharge
Transport - Rs. 75 - Auto fare

This single structure is the backbone of the entire project. The diagram below shows what it actually looks like in memory: the list holds three slots (here showing the first three expenses), and each slot does not hold a number — it holds a whole dictionary, with its own three labelled fields sitting together inside it.

expenses = [ ... ] — a list of dictionaries expenses[0] (a dictionary) "amount": 50 "category": "Transport" "note": "Bus fare" expenses[1] (a dictionary) "amount": 120 "category": "Food" "note": "Canteen lunch" expenses[2] (a dictionary) "amount": 30 "category": "Food" "note": "Evening snack" Sorting or deleting a slot now moves the whole dictionary — amount, category and note can never go out of sync.

Compare this picture with the parallel-list trap from Step 3. If you sort this expenses list by amount, each element that moves is an entire dictionary — amount, category, and note move as one bundle. There is no way for the category to get separated from its amount, because they were never in separate containers to begin with.

Step 6: Wrapping actions in functions

Right now, "add an expense" means writing out a dictionary literal by hand every time, which is repetitive and error-prone. Wrap it in a function instead:

def add_expense(expense_list, amount, category, note):
    new_expense = {"amount": amount, "category": category, "note": note}
    expense_list.append(new_expense)

expenses = []
add_expense(expenses, 50, "Transport", "Bus fare")
add_expense(expenses, 120, "Food", "Canteen lunch")
print(len(expenses))

Trace it: expenses starts empty (len would be 0). The first call builds a dictionary and appends it, so len(expenses) becomes 1. The second call appends a second dictionary, so len(expenses) becomes 2. The program prints 2. Notice that add_expense never needs to know how many expenses already exist — append always adds to the end, and the list keeps its own count. This is exactly the scaling property that manually numbered variables (Step 1) could never give us.

Step 7: Totals — the accumulator pattern, twice

We already summed a plain list of numbers in Step 2. Summing a list of dictionaries uses the identical pattern — start a counter at zero, and add one field from each item as you loop:

def get_total(expense_list):
    total = 0
    for e in expense_list:
        total = total + e["amount"]
    return total

Using the five-expense list from Step 5, this returns 475 rupees, by the same running total you already traced in Step 2 — the only difference is that each amount now comes from e["amount"] instead of directly from a list. Once this pattern is familiar, Python lets you write it in one line using sum() with a generator expression: sum(e["amount"] for e in expense_list) produces the identical 475. Use whichever version you can explain out loud — the one-liner is not "better," it is just shorter once you already understand the loop it replaces.

Category-wise totals need a small but important twist: instead of one running total, we need many running totals — one per category — and we don't know the category names in advance. A dictionary is again the right tool, this time used as an accumulator rather than a single record:

def get_category_totals(expense_list):
    totals = {}
    for e in expense_list:
        cat = e["category"]
        amt = e["amount"]
        if cat in totals:
            totals[cat] = totals[cat] + amt
        else:
            totals[cat] = amt
    return totals

Trace this carefully against the five-expense list, because the accumulator pattern appears everywhere in real programs, not just expense trackers. totals starts as an empty dictionary {}.

e = Transport/50  => "Transport" not in totals => totals = {"Transport": 50}
e = Food/120      => "Food" not in totals      => totals = {"Transport": 50, "Food": 120}
e = Food/30       => "Food" IS in totals       => totals["Food"] = 120 + 30 = 150
e = Recharge/200  => "Recharge" not in totals  => totals = {"Transport": 50, "Food": 150, "Recharge": 200}
e = Transport/75  => "Transport" IS in totals  => totals["Transport"] = 50 + 75 = 125

The function returns {"Transport": 125, "Food": 150, "Recharge": 200}. As a sanity check, always verify that the category totals add up to the grand total: 125 + 150 + 200 = 475, which matches get_total from earlier. This cross-check — does the breakdown sum to the total? — is exactly how you catch bugs in your own accumulator logic, and it is worth doing by hand every single time you write one. Once the if/else version makes sense, Python's dictionaries offer a shortcut method, .get(key, default), which returns the default value when the key is missing instead of raising an error: totals[cat] = totals.get(cat, 0) + amt does the identical job as the four-line if/else block, in one line.

Step 8: Making it interactive with a menu loop

A real tool has to run continuously, accept typed input, and let the user pick what to do next. That is a while True loop wrapped around an if/elif chain, with a way to break out:

def add_expense(expense_list, amount, category, note):
    new_expense = {"amount": amount, "category": category, "note": note}
    expense_list.append(new_expense)

def show_all(expense_list):
    if len(expense_list) == 0:
        print("No expenses recorded yet.")
        return
    for i, e in enumerate(expense_list):
        print(i + 1, ".", e["category"], "- Rs.", e["amount"], "-", e["note"])

def get_total(expense_list):
    return sum(e["amount"] for e in expense_list)

def get_category_totals(expense_list):
    totals = {}
    for e in expense_list:
        totals[e["category"]] = totals.get(e["category"], 0) + e["amount"]
    return totals

expenses = []

while True:
    print("\n1. Add expense")
    print("2. Show all expenses")
    print("3. Show total")
    print("4. Show category-wise totals")
    print("5. Exit")
    choice = input("Choose an option: ")

    if choice == "1":
        category = input("Category: ")
        note = input("What was it for? ")
        amount = float(input("Amount in Rs.: "))
        if amount > 0:
            add_expense(expenses, amount, category, note)
            print("Expense added.")
        else:
            print("Amount must be positive.")
    elif choice == "2":
        show_all(expenses)
    elif choice == "3":
        print("Total spent: Rs.", get_total(expenses))
    elif choice == "4":
        for cat, amt in get_category_totals(expenses).items():
            print(cat, ": Rs.", amt)
    elif choice == "5":
        print("Goodbye!")
        break
    else:
        print("Invalid choice, try again.")

Walk through what happens if a user types 1, enters Food, Samosa, and 25, then types 3, then types 5. The first pass through the loop matches choice == "1", reads three inputs, checks amount > 0 (25 > 0 is true), and calls add_expense, printing "Expense added." The loop does not end — it goes straight back to printing the menu, because it is wrapped in while True. The second pass matches choice == "3" and prints Total spent: Rs. 25.0 (note the .0float() always produces a decimal value, even from whole-number input, which is worth pointing out explicitly since it surprises many students the first time they see it). The third pass matches choice == "5", prints "Goodbye!", and break exits the loop, ending the program. Without that break, the loop would run forever, since nothing else stops it — this is a second common bug worth watching for: forgetting to break out of a menu loop leaves the program stuck printing the menu after every single action.

Also notice the validation check on the amount. If a user mistypes and enters -50, the program does not silently accept a negative expense — it refuses and asks again on the next loop pass. A tracker that let expenses go negative would quietly corrupt every total and category sum computed afterward, so this one if check is protecting every calculation downstream of it, not just this one input.

Step 9: A first look at making data survive

Everything above has one real limitation: the moment the program ends, expenses disappears, because it only ever lived in the computer's memory. To make expenses survive between runs, they need to be written to a file on disk. This is a brief preview of file handling, which you will study in much more depth later, but the core idea is simple enough to use right now:

def save_to_file(expense_list, filename):
    file = open(filename, "a")
    for e in expense_list:
        line = e["category"] + "," + str(e["amount"]) + "," + e["note"] + "\n"
        file.write(line)
    file.close()

save_to_file(expenses, "expenses.txt")

open(filename, "a") opens the file in append mode — if expenses.txt already exists, new lines are added at the end instead of erasing what was there. Each dictionary is turned into one comma-separated line of plain text using str(e["amount"]) to convert the number into text before joining it with commas, and "\n" to move to the next line. Running save_to_file on our five-expense list produces a file containing exactly five lines, the first of which reads Transport,50,Bus fare. This is the same idea behind the CSV export button you have probably seen in real banking or UPI apps — a list of records, written out one line per record, fields separated by commas.

Putting the design decisions in order

Step back and notice the shape of what you just built, because this shape — not the expense tracker specifically — is the actual lesson. You never chose a data structure because it was "the correct answer." Each choice was forced by a concrete failure of the previous one: separate numbered variables couldn't be counted or looped over, so a list replaced them. A single list of amounts couldn't hold a category and a note alongside each amount, so parallel lists were tried — and those silently broke the moment anything got reordered, so a dictionary replaced them, bundling one expense's fields together. A single dictionary could only ever represent one expense, so a list of dictionaries combined both ideas. And a growing list of dictionaries needed one more thing before it was a usable tool for another human being: a menu loop to drive it interactively, and a save function so the work was not lost when the program closed. Every real application you will ever build — a to-do list, an attendance register, a library catalogue — follows this exact same chain of reasoning.

Check your understanding

  • Given expenses = [{"amount": 40, "category": "Food", "note": "Tea"}, {"amount": 60, "category": "Food", "note": "Biscuits"}, {"amount": 90, "category": "Transport", "note": "Metro"}], trace get_category_totals(expenses) by hand, one dictionary at a time, the way Step 7 was traced. What does the function return, and does it sum to 190?
  • Explain in your own words why amounts = [50, 120, 30] paired with categories = ["Transport", "Food", "Food"] is riskier than a list of dictionaries, using the word "sort" or "delete" in your answer.
  • Write a function get_average_expense(expense_list) that returns the average amount per expense. What should it return for an empty list, and why must you check for that case before dividing?
  • Write a function highest_category(expense_list) that returns the name of the category with the largest total. (Hint: start from get_category_totals, then loop over its keys and values to find the maximum — do not try to solve both problems in one loop on your first attempt.)
  • Extend the menu program with a sixth option, "Delete an expense by number," using show_all's numbering as a guide. What Python list method removes an item by its position?

Summary

A Personal Expense Tracker is built from four ideas you already know, combined in one specific order: a dictionary bundles the several fields of one expense (amount, category, note) so they can never be separated by an operation like sorting; a list holds a growable, countable sequence of those dictionaries; a for loop visits every expense to compute a running total, using the same accumulator pattern whether you're summing a plain list of numbers or reading one field out of each dictionary in a list; and a second dictionary, used as an accumulator rather than a single record, produces a category-wise breakdown whose values must always sum back to the grand total. A while True loop with an if/elif chain and an explicit break turns these functions into an interactive program, an if amount > 0 check protects every downstream total from corrupted input, and open(filename, "a") is the simplest possible way to make the data outlive the program that created it. The parallel-list trap — storing related fields in separate, same-length lists — is the most common and most dangerous mistake at this stage precisely because it fails silently instead of crashing, so always prefer one dictionary per record the moment a record needs more than one field.

Think About It

Think about this: How would you explain project: personal expense tracker to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

← Project: Python Quiz ApplicationProject: Weather Dashboard with API →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn