Record spending, save it to a CSV file, load it back, see where the money went.
/build/project-expense-tracker-csv with the starter and the rubric already in it.Record a student's expenses, refuse bad amounts, save them to expenses.csv with the csv module, load them back, and print this month's total and a per-category breakdown with the biggest category's share.
Saving and loading data is the step that turns a script into a tool. The csv module handles the cases a string join gets wrong, like a note with a comma in it, and you will see exactly why.
You can validate input, write and read a file with the csv module, and summarise rows with a dict.
Each step is one function in expenses.py. In the workspace, press Check next to a step: it runs your file and tells you exactly what is still wrong.
Build against this exact list — it is the spec your published project is judged on.
Copy this into a new file — or open it in a workspace as expenses.py. The TODO parts are yours to fill.
"""
Expense tracker with CSV.
Records expenses, saves them to expenses.csv, loads them back and prints
where the money went. Uses only the csv module from the standard library.
Fill in one function per step, press Run, then press "Check step".
"""
import csv
from pathlib import Path
CSV_PATH = Path("expenses.csv")
FIELDS = ["date", "category", "amount", "note"]
SEED = [
("2026-08-28", "food", 240, "canteen thali"),
("2026-08-30", "travel", 60, "metro card top-up"),
("2026-09-01", "rent", 6500, "PG rent share"),
("2026-09-02", "food", 180, "chai + samosa x6"),
("2026-09-04", "books", 450, "DSA book, second hand"),
("2026-09-05", "travel", 120, "auto to college"),
("2026-09-07", "food", 320, "birthday treat"),
("2026-09-09", "recharge", 299, "Jio monthly"),
("2026-09-12", "food", 210, "mess extra"),
]
def add_expense(rows: list[dict], date: str, category: str, amount: float, note: str = "") -> dict:
# TODO: refuse amount <= 0 with ValueError. Otherwise build
# {"date", "category" (stripped + lowercase), "amount" (float), "note"},
# append it to rows and return it.
raise NotImplementedError("step 1, add_expense(): validate and append one row")
def save_csv(rows: list[dict], path: Path = CSV_PATH) -> None:
# TODO: open(path, "w", newline="", encoding="utf-8"), then
# csv.DictWriter(f, fieldnames=FIELDS) -> writeheader() -> writerows(rows)
raise NotImplementedError("step 2, save_csv(): write rows with a header")
def load_csv(path: Path = CSV_PATH) -> list[dict]:
# TODO: return [] if the file does not exist. Otherwise read it with
# csv.DictReader. CSV gives back strings, so turn "amount" into a float.
raise NotImplementedError("step 3, load_csv(): read the rows back")
def monthly_total(rows: list[dict], month: str) -> float:
# TODO: sum the amounts whose date starts with month, e.g. "2026-09"
raise NotImplementedError("step 4, monthly_total(): one month's spending")
def total_by_category(rows: list[dict]) -> dict[str, float]:
# TODO: {category: total}, biggest total first.
# Hint: sorted(totals.items(), key=lambda kv: kv[1], reverse=True)
raise NotImplementedError("step 5, total_by_category(): totals, biggest first")
def main() -> None:
rows: list[dict] = []
for date, cat, amount, note in SEED:
add_expense(rows, date, cat, amount, note)
save_csv(rows)
rows = load_csv()
print(f"Loaded {len(rows)} expenses from {CSV_PATH}")
print(f"September 2026: ₹{monthly_total(rows, '2026-09'):,.0f}")
totals = total_by_category(rows)
for cat, amt in totals.items():
print(f" {cat:<10} ₹{amt:>8,.0f}")
# TODO: print the biggest category and its share of all spending:
# "Biggest category: rent (77.6% of ₹8,379)"
raise NotImplementedError("step 6, main(): print the biggest category")
if __name__ == "__main__":
try:
main()
except NotImplementedError as todo:
# A fresh starter is SUPPOSED to stop here. Say which step is next
# instead of printing a traceback that looks like a bug.
print(f"Not built yet: {todo}")
print("Write that function, then press Run again. Each step you finish moves this message forward.")
Try every step first. This version passes all 6 checks; yours can look different and still pass.
"""
Expense tracker with CSV.
Records expenses, saves them to expenses.csv, loads them back and prints
where the money went. Uses only the csv module from the standard library.
"""
import csv
from pathlib import Path
CSV_PATH = Path("expenses.csv")
FIELDS = ["date", "category", "amount", "note"]
SEED = [
("2026-08-28", "food", 240, "canteen thali"),
("2026-08-30", "travel", 60, "metro card top-up"),
("2026-09-01", "rent", 6500, "PG rent share"),
("2026-09-02", "food", 180, "chai + samosa x6"),
("2026-09-04", "books", 450, "DSA book, second hand"),
("2026-09-05", "travel", 120, "auto to college"),
("2026-09-07", "food", 320, "birthday treat"),
("2026-09-09", "recharge", 299, "Jio monthly"),
("2026-09-12", "food", 210, "mess extra"),
]
def add_expense(rows: list[dict], date: str, category: str, amount: float, note: str = "") -> dict:
if amount <= 0:
raise ValueError("amount must be more than 0")
row = {"date": date, "category": category.strip().lower(), "amount": float(amount), "note": note}
rows.append(row)
return row
def save_csv(rows: list[dict], path: Path = CSV_PATH) -> None:
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
writer.writerows(rows)
def load_csv(path: Path = CSV_PATH) -> list[dict]:
if not Path(path).exists():
return []
with open(path, newline="", encoding="utf-8") as f:
return [{**r, "amount": float(r["amount"])} for r in csv.DictReader(f)]
def total_by_category(rows: list[dict]) -> dict[str, float]:
totals: dict[str, float] = {}
for r in rows:
totals[r["category"]] = totals.get(r["category"], 0) + r["amount"]
return dict(sorted(totals.items(), key=lambda kv: kv[1], reverse=True))
def monthly_total(rows: list[dict], month: str) -> float:
return sum(r["amount"] for r in rows if r["date"].startswith(month))
def main() -> None:
rows: list[dict] = []
for date, cat, amount, note in SEED:
add_expense(rows, date, cat, amount, note)
save_csv(rows)
rows = load_csv()
print(f"Loaded {len(rows)} expenses from {CSV_PATH}")
print(f"September 2026: ₹{monthly_total(rows, '2026-09'):,.0f}")
totals = total_by_category(rows)
for cat, amt in totals.items():
print(f" {cat:<10} ₹{amt:>8,.0f}")
grand = sum(totals.values())
top = next(iter(totals))
print(f"Biggest category: {top} ({totals[top] / grand * 100:.1f}% of ₹{grand:,.0f})")
if __name__ == "__main__":
main()
expenses.py and a README.md holding all 5 rubric items as a checklist.pyrun.in/u/<handle>/w/project-expense-tracker-csv.pyrun.in/u/<handle> portfolio page.Your workspace saves to this browser only. Sign in to keep it across devices and to publish it.
Flip the workspace to Public and paste its URL below, or push the file to a GitHub gist or repo. Paste the code inline too if you want the optional AI review to comment on specific lines.
A published PyRun workspace URL (pyrun.in/u/<handle>/w/project-expense-tracker-csv) works as the public URL too.