Python Tutorial
Python Project: Expense Tracker
Record amounts by category, print a table, and compute totals and the largest spend.
What you are building
You record expenses as pairs: a category and an amount. The program prints a table of every row, a total for each category, the grand total, and the single largest spend.
Work in the Python editor at /try. Leave HTML, C, and C++ for their own tracks. The numbers are listed in the script so Run prints the report at once.
Skills used
- A list of
(category, amount)tuples - A loop that prints a table of rows
- A dict that sums amounts per category
sumfor the grand totalmaxwith a key to find the largest row
Store rows and print a table
A tuple is enough for one spend: the category does not change, the amount does not change. Keep all rows in one list. Print a header, then each pair, then a count of rows.
Example
expenses = [
("Food", 12.50),
("Transit", 3.20),
("Food", 8.75),
("Books", 24.00),
]
print(f"{'category':<12} {'amount':>8}")
for category, amount in expenses:
print(f"{category:<12} {amount:>8.2f}")
print("Rows:", len(expenses))Two Food rows stay two rows. Do not merge them yet. The table is the ledger. Totals come next, from the same list.
Totals by category
Walk the list once. For each category, add the amount to a dict. Missing keys start at 0. The grand total is the sum of the amounts, not a second pass over the dict, unless you prefer that.
Example
expenses = [
("Food", 12.50),
("Transit", 3.20),
("Food", 8.75),
("Books", 24.00),
]
by_cat = {}
for category, amount in expenses:
by_cat[category] = by_cat.get(category, 0) + amount
print(by_cat)
print("Grand total:", sum(amount for _, amount in expenses))Use amount as a float, not a string. If you later read text, wrap it withfloat(...) once when you build the list. Adding strings would concatenate, and the total would be nonsense.
Complete program
The full report prints the ledger, the category totals, the largest single row, and the grand total. Run it at /try and check the Books line: 24.00 is the largest spend.
Example
expenses = [
("Food", 12.50),
("Transit", 3.20),
("Food", 8.75),
("Books", 24.00),
("Transit", 3.20),
("Food", 15.10),
]
def print_table(rows):
print(f"{'category':<12} {'amount':>8}")
print("-" * 21)
for category, amount in rows:
print(f"{category:<12} {amount:>8.2f}")
by_cat = {}
for category, amount in expenses:
by_cat[category] = by_cat.get(category, 0) + amount
largest = max(expenses, key=lambda row: row[1])
grand = sum(amount for _, amount in expenses)
print("Expense tracker")
print()
print_table(expenses)
print()
print("Totals by category")
print(f"{'category':<12} {'total':>8}")
print("-" * 21)
for category in sorted(by_cat):
print(f"{category:<12} {by_cat[category]:>8.2f}")
print()
print("Largest spend:", largest[0], f"{largest[1]:.2f}")
print("Grand total: ", f"{grand:.2f}")
print("Row count: ", len(expenses))| Category | Total |
|---|---|
| Books | 24.00 |
| Food | 36.35 |
| Transit | 6.40 |
| Grand total | 66.75 |
Food is 12.50 + 8.75 + 15.10. Transit is 3.20 twice. Sorted category names keep the second table stable from run to run.
Common mistakes
- Using
max(expenses)without a key. Tuples compare the category string first, so Books would not always win. - Rounding each row before you add. Round when you print. Sum the original amounts.
- Updating
by_catwith=instead of+. The second Food row would replace the first instead of adding to it. - Printing money with
print(amount)only.8.75is fine;3.2looks better as3.20with.2f. - Mixing currencies in one list with no tag. This project assumes one unit, such as dollars.
How to extend / Practice tasks
Keep the six demo rows until the 66.75 total matches.
- Add
("Food", 4.15)and confirm the Food total becomes 40.50 and the grand total becomes 70.90. - Print the category with the highest total, not only the highest single row. Use
max(by_cat, key=by_cat.get). - Filter the ledger to one category before printing: a helper
only(rows, name)that returns matching tuples, then print Food only.
📘 Real-World Deep Dive
An expense tracker is where you first feel the payoff of grouping and aggregation: raw rows in, totals-per-category out. That "reduce a list of records to a summary" move is the heart of every report and dashboard.
What to build
Read a list of expenses, total them per category, and print a sorted summary with the grand total — the CLI ancestor of every budgeting app.
Real-Life Example
from collections import defaultdict
expenses = [
("food", 12.50), ("transport", 3.00), ("food", 8.25),
("rent", 900.00), ("transport", 2.75),
]
totals = defaultdict(float)
for category, amount in expenses:
totals[category] += amount
for category, total in sorted(totals.items(), key=lambda kv: -kv[1]):
print(f"{category:10} {total:8.2f}")
print(f"{'TOTAL':10} {sum(totals.values()):8.2f}")defaultdict(float) removes the "is this the first time I have seen this category?" check entirely.
Expected Output
rent 900.00
food 20.75
transport 5.75
TOTAL 926.50Common mistakes
- Storing money as
floataccumulates rounding errors; for real accounting usedecimal.Decimalor store integer cents. dict[key] += amountthrowsKeyErroron a new key — that's exactly whatdefaultdict(orCounter) prevents.- Sorting by category name when the user wants "biggest spend first" — sort by value, as above.
🚀 Performance & Best Practices
- One pass builds every total (O(n)); no need to scan the list once per category.
collections.Countercan sum too, and its.most_common()gives the sorted ranking for free.- Keep parsing (reading CSV rows) separate from aggregating so you can test the math on hand-made data.
🧪 Try It Yourself
- Load the expenses from a CSV file with
csv.readerinstead of a hard-coded list. - Add a month filter so the report only totals rows in a chosen month.
- Switch the totals to
Decimaland prove the grand total is exact to the cent.