ASAmol Shukla
Projects
Courses
Prompts
Skills
Contact
Resume
Course Outline
Syllabus Overview

Complete Python Course: From Zero to Professional

Courses/Complete Python Course: From Zero to Professional/Lesson 25: Capstone — Build a CLI Expense Tracker
90 mins lesson duration•14 mins read

Lesson 25: Capstone — Build a CLI Expense Tracker

Bring everything together: functions, dicts, files, JSON, and a menu loop in one complete project.

The Capstone Project

You have learned variables, control flow, data structures, functions, OOP, files, errors, and JSON. Now we combine all of it into a real, usable program: a CLI Expense Tracker. This is the moment you stop being a beginner.

This project mirrors how real software is built: a small set of focused functions, persistence to disk, validation of user input, and a menu loop. If you can build this from memory, you are ready to start building your own projects.

What You'll Learn in This Lesson

  • Design a small program's architecture before writing code
  • Persist data to JSON so it survives restarts
  • Validate user input with try/except
  • Structure a menu-driven CLI application

Features We Build

Feature What it does Skills used
Add Add an expense (amount + category) Functions, validation
List / Summary Total spent + per-category totals Dicts, loops
Persistence Saved to expenses.json JSON, files
Validation Reject invalid amounts try/except, raise

Architecture — Think Before You Code

The whole program is a small set of focused functions:

Function Responsibility
load_expenses() Read JSON from disk (or return [])
save_expenses(expenses) Write the list to JSON
add_expense(expenses, amount, category) Validate + append
print_summary(expenses) Aggregate totals by category
main() The while True menu loop

Each function does one job — the golden rule of function design (Lesson 12).

Mental model: map each menu option to the function it calls. The menu is the front door; the functions are the rooms behind it. main() stays thin — it only routes.


Building It Step by Step

Step 1 — Load: if expenses.json doesn't exist, start empty. Otherwise json.load it.

def load_expenses():
    if not os.path.exists(DATA_FILE):
        return []
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)

Step 2 — Save: dump the list with indent=2 for readable JSON.

Step 3 — Add with validation: reject non-positive amounts by raising ValueError.

Step 4 — Summary: a dict of totals using .get() — exactly the pattern from Lesson 9:

by_category = {}
for e in expenses:
    by_category[e["category"]] = by_category.get(e["category"], 0) + e["amount"]

Step 5 — The menu loop: while True + break from Lesson 5, with try/except around input:

while True:
    print("\n1. Add expense  2. Summary  3. Exit")
    choice = input("Choose: ")
    if choice == "1":
        try:
            amount = float(input("Amount: "))
            category = input("Category: ").strip()
            add_expense(expenses, amount, category)
            save_expenses(expenses)
        except ValueError as e:
            print("Invalid input:", e)
    elif choice == "2":
        print_summary(expenses)
    elif choice == "3":
        print("Goodbye!")
        break
    else:
        print("Unknown choice")

How This Course Prepared You

Every line of this project uses a lesson you already completed:

  • while True + break → Lesson 5
  • Dicts + .get() → Lesson 9
  • Functions → Lesson 12
  • try/except + raise → Lesson 21
  • JSON + files → lessons 20 & 23
  • The __name__ guard → Lesson 15

That is the point: professional Python is just these building blocks arranged well.


Testing Your Capstone

Apply Lesson 24's habits to the project:

def test_add_expense():
    expenses = []
    add_expense(expenses, 250, "food")
    assert expenses == [{"amount": 250, "category": "food"}]

def test_add_rejects_zero():
    expenses = []
    try:
        add_expense(expenses, 0, "food")
    except ValueError:
        pass
    else:
        raise AssertionError("Expected ValueError for amount 0")

Extension Ideas

Extension Skills practiced
Delete an expense Lists + pop()
Export to CSV The csv module
Budget alert Conditionals + logging
Date tracking datetime + dicts
Refactor to classes OOP from Module 5

Common Mistakes to Avoid

  • Mistake: One giant main() with everything inline — Fix: split into focused functions.
  • Mistake: Letting bad input crash the menu — Fix: wrap input parsing in try/except.
  • Mistake: Forgetting to save after adding — Fix: call save_expenses() after every mutation.
  • Mistake: float(input(...)) on empty input crashing — Fix: validate and handle ValueError.
  • Mistake: Reading the JSON file before it exists — Fix: check os.path.exists first (or catch FileNotFoundError).

Professional Tips & Tricks

  • Keep main() thin: it only routes menu choices to functions — each function does one job.
  • The try/except around input makes the program survive bad data — test it with 'abc'.
  • Extend the capstone with CSV export and a delete option to practice everything again.
  • Write a few pytest tests for add_expense — tests make the project feel professional.
  • Use __name__ == "__main__" so the module is also importable.

Key Takeaways

  • Design the function list before writing code.
  • JSON gives your program memory across runs.
  • Validate all user input with try/except.
  • One function = one job; main() only routes.
  • Test the core functions with pytest.
  • You are no longer a beginner. 🎉

Next steps: Review the syllabus, redo any lesson, or explore the Applied Data Science course in the Learning Hub.

Interactive Lesson Code Snippet
import json
import os

DATA_FILE = "expenses.json"


def load_expenses():
    """Load expenses from JSON, or start empty."""
    if not os.path.exists(DATA_FILE):
        return []
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def save_expenses(expenses):
    """Persist expenses to JSON."""
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(expenses, f, indent=2)


def add_expense(expenses, amount, category):
    """Validate and append one expense."""
    if amount <= 0:
        raise ValueError("Amount must be positive")
    expenses.append({"amount": amount, "category": category})
    print(f"Added Rs.{amount} to {category}")


def print_summary(expenses):
    """Print total and per-category totals."""
    if not expenses:
        print("No expenses yet.")
        return
    total = sum(e["amount"] for e in expenses)
    print(f"Total spent: Rs.{total}")
    by_category = {}
    for e in expenses:
        by_category[e["category"]] = by_category.get(e["category"], 0) + e["amount"]
    for cat, amt in by_category.items():
        print(f"  {cat}: Rs.{amt}")


def main():
    expenses = load_expenses()
    while True:
        print("\n1. Add expense  2. Summary  3. Exit")
        choice = input("Choose: ")
        if choice == "1":
            try:
                amount = float(input("Amount: "))
                category = input("Category: ").strip()
                add_expense(expenses, amount, category)
                save_expenses(expenses)
            except ValueError as e:
                print("Invalid input:", e)
        elif choice == "2":
            print_summary(expenses)
        elif choice == "3":
            print("Goodbye!")
            break
        else:
            print("Unknown choice")


if __name__ == "__main__":
    main()
Language: python

Lesson Code (Python)

import json
import os

DATA_FILE = "expenses.json"


def load_expenses():
    """Load expenses from JSON, or start empty."""
    if not os.path.exists(DATA_FILE):
        return []
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def save_expenses(expenses):
    """Persist expenses to JSON."""
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(expenses, f, indent=2)


def add_expense(expenses, amount, category):
    """Validate and append one expense."""
    if amount <= 0:
        raise ValueError("Amount must be positive")
    expenses.append({"amount": amount, "category": category})
    print(f"Added Rs.{amount} to {category}")


def print_summary(expenses):
    """Print total and per-category totals."""
    if not expenses:
        print("No expenses yet.")
        return
    total = sum(e["amount"] for e in expenses)
    print(f"Total spent: Rs.{total}")
    by_category = {}
    for e in expenses:
        by_category[e["category"]] = by_category.get(e["category"], 0) + e["amount"]
    for cat, amt in by_category.items():
        print(f"  {cat}: Rs.{amt}")


def main():
    expenses = load_expenses()
    while True:
        print("\n1. Add expense  2. Summary  3. Exit")
        choice = input("Choose: ")
        if choice == "1":
            try:
                amount = float(input("Amount: "))
                category = input("Category: ").strip()
                add_expense(expenses, amount, category)
                save_expenses(expenses)
            except ValueError as e:
                print("Invalid input:", e)
        elif choice == "2":
            print_summary(expenses)
        elif choice == "3":
            print("Goodbye!")
            break
        else:
            print("Unknown choice")


if __name__ == "__main__":
    main()

Console Output

1. Add expense  2. Summary  3. Exit
Choose: 1
Amount: 250
Category: food
Added Rs.250.0 to food

1. Add expense  2. Summary  3. Exit
Choose: 1
Amount: 1200
Category: travel
Added Rs.1200.0 to travel

1. Add expense  2. Summary  3. Exit
Choose: 2
Total spent: Rs.1450.0
  food: Rs.250.0
  travel: Rs.1200.0

1. Add expense  2. Summary  3. Exit
Choose: 3
Goodbye!

Code Visualization Tips

  • 🧠Map each menu option to the function it calls — draw the call flow as a small diagram.
  • 🧠Visualize expenses.json as the program's memory between runs: a box that survives restarts.
  • 🧠Trace one full cycle: input -> function -> dict/list update -> save to file -> reload on next run.

Professional Tips & Tricks

  • ⚡Keep main() thin: it only routes menu choices to functions — each function does one job.
  • ⚡The try/except around input makes the program survive bad data — test it with 'abc'.
  • ⚡Extend the capstone with CSV export and a delete option to practice everything again.

Python Code Judge & Practice Arena

LeetCode Style

Run real Python 3.12 WebAssembly code directly in your browser against automated test suites.

Solved:0 / 20
0 / 370 XP
Challenges:
Problem 1 of 20

Problem 1: Expense Manager Engine 1

Easy+10 XP
Implement class `ExpenseManager_1` with `add_expense(category, amount)`, `get_total()`, and `get_category_report()` returning a dict of category sums sorted in descending order of spend.
Sample Test Cases:
Input: (lambda m: (m.add_expense('Food', 50), m.add_expense('Tech', 150), m.get_total())[2])(ExpenseManager_1())
Expected: 200.0
Input: (lambda m: (m.add_expense('Food', 50), m.add_expense('Tech', 150), m.get_category_report())[2])(ExpenseManager_1())
Expected: {'Tech': 150.0, 'Food': 50.0}
main.pyPython 3.12 (WASM)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Press Run Code to test or Submit to verify test cases

Test Your Knowledge

Instant feedback

Quick Check: Lesson 25: Capstone Project: CLI Expense Tracker & Personal Finance Manager

1 / 20
What dataclass schema cleanly models an Expense entry? from dataclasses import dataclass from datetime import datetime @dataclass class Expense: id: int category: str amount: float date: str

Course complete

You finished Complete Python Course!

Review the full syllabus, revisit any lesson, or explore another course in the Learning Hub.

View full syllabusBrowse all courses
Previous: Testing & Debugging Like a Pro
Made withbyAmol Shukla·amolshukla.online
ASAmol Shukla

AI Developer, Trainer & Agentic AI Expert building practical learning systems and real-world AI applications.

Explore

  • Projects
  • Courses
  • Prompts
  • Skills
  • Contact
  • Experience
  • Blogs

Connect

  • Resume
  • Contact
© 2026 Amol Shukla·Created withbyamolshukla.online
Back to top