Case Study 1: Building a Personal Budget Tracker


Overview

Builder: Marcus, a freelance graphic designer who tracks expenses in a spreadsheet Goal: Replace a manual spreadsheet workflow with a command-line budget tracker AI Tool: Claude Code Session Duration: Approximately 90 minutes Lines of Code Produced: ~350 Prior Python Experience: Completed Chapter 5; can read Python but has never written a program from scratch


The Starting Point

Marcus has been tracking his income and expenses in a spreadsheet for two years. Every month, he manually types each transaction, creates formulas for subtotals, and copies the same layout to a new sheet. The process is tedious and error-prone --- last month, he accidentally deleted a formula and his totals were wrong for three weeks before he noticed.

He wants a simple program he can run from the terminal to log transactions and see summaries. He has read Chapters 1 through 5 of this book and completed the task manager exercise in Chapter 6. He knows the vibe coding workflow: plan first, generate incrementally, iterate, and test.


The Planning Conversation

Marcus opens Claude Code and starts with a planning prompt:

I want to build a personal budget tracker as a CLI tool in Python.
I currently track income and expenses in a spreadsheet. I want to be
able to:

1. Log income transactions (amount, category like "salary" or
   "freelance", description)
2. Log expense transactions (amount, category like "groceries" or
   "rent", description)
3. See my current balance (total income minus total expenses)
4. See a breakdown of spending by category
5. See a monthly summary
6. Save everything to a JSON file

Please help me plan the data model and command structure.

The AI responds with a structured plan:

  • A Transaction dataclass with fields for id, amount (positive for income, negative for expenses), category, description, transaction_date, transaction_type, and created_at
  • A BudgetTracker class that manages the list of transactions with methods for adding, querying, and summarizing data
  • CLI commands: income, expense, balance, summary, monthly, list, delete
  • JSON file persistence using the same pattern as the task manager

Marcus likes the plan but notices the AI suggested using a class instead of standalone functions. He asks about this:

In the task manager chapter, we used standalone functions. Why are you
suggesting a class here? What is the advantage?

The AI explains that the BudgetTracker class encapsulates the data path and loaded transactions, avoiding the need to reload from disk on every function call. For an application where multiple operations might happen in sequence (like adding several transactions), this is more efficient. Marcus decides to go with the class approach since it was covered in Chapter 5, Section 5.8.


Building the Data Model

Marcus asks for the data model:

Create the Transaction dataclass. Income amounts should be stored as
positive numbers and expense amounts as negative numbers. Include a
transaction_date field that defaults to today's date in YYYY-MM-DD format.
Use type hints and docstrings.

The AI produces a clean dataclass:

@dataclass
class Transaction:
    id: int
    amount: float
    category: str
    description: str
    transaction_date: str = field(
        default_factory=lambda: date.today().isoformat()
    )
    transaction_type: str = "expense"
    created_at: str = field(
        default_factory=lambda: datetime.now().isoformat()
    )

Marcus notices the amount is always stored as a float, but the transaction_type field separately tracks whether it is income or expense. He asks the AI to clarify whether the sign convention (positive for income, negative for expenses) is enforced anywhere. The AI realizes it is not, so Marcus asks for explicit enforcement:

Add methods add_income() and add_expense() that ensure the sign is
correct. add_income() should always store a positive amount. add_expense()
should always store a negative amount. Both should validate that the
input amount is positive (the user should always enter a positive number;
the function handles the sign).

This is a key learning moment. Marcus caught a potential bug before it happened by carefully reading the AI's output and asking about the design intent. This is the "evaluate" step of the vibe coding workflow in action.


Building the Summary Features

The feature Marcus is most excited about is the spending breakdown. He sends a detailed prompt:

Write a get_summary_by_category() method that groups transactions by
category and returns a dictionary mapping category names to total amounts.
It should have an optional filter for transaction_type so I can see just
expenses or just income. Sort the results by amount, highest first.

The AI produces:

def get_summary_by_category(
    self,
    transaction_type: Optional[str] = None,
) -> dict[str, float]:
    filtered = self.transactions
    if transaction_type:
        filtered = [
            t for t in filtered
            if t.transaction_type == transaction_type
        ]
    summary: dict[str, float] = {}
    for txn in filtered:
        summary[txn.category] = summary.get(txn.category, 0) + abs(txn.amount)
    return dict(sorted(summary.items(), key=lambda x: x[1], reverse=True))

Marcus tests it with sample data and then asks for a visual display:

Make the display_summary() method show a simple text-based bar chart
next to each category, using # characters to represent the percentage
of total spending. Like this:

  Expense Breakdown:
    rent            $1200.00   64.5%  #############
    groceries        $435.00   23.4%  #####
    utilities        $120.00    6.5%  #
    entertainment     $60.00    3.2%  #
    transport         $45.00    2.4%

The AI implements the bar chart using int(pct / 5) to determine the number of # characters, producing a proportional visual representation. Marcus is pleased with the result --- it gives him an instant overview of where his money goes.


The Monthly Summary

For the monthly summary, Marcus asks:

Add a get_monthly_summary() method that takes a year and month number
and returns a dictionary with total income, total expenses, net
(income minus expenses), the number of transactions, and an expense
breakdown by category for that month.

The AI uses string matching on the transaction_date field (t.transaction_date.startswith(month_str)) to filter transactions for a given month. Marcus tests it with January 2025 data:

$ python budget.py monthly 2025 1
January 2025 Summary:
  Income:   $4000.00
  Expenses: $1860.00
  Net:      $2140.00
  Transactions: 8

This is exactly the kind of summary that took Marcus 20 minutes to compute manually from his spreadsheet.


Error Handling and Edge Cases

Marcus applies the lesson from Chapter 6 about explicitly requesting error handling:

Add validation for these cases:
1. Income and expense amounts must be positive numbers
2. Category names should be converted to lowercase for consistency
3. Date strings should be validated as proper YYYY-MM-DD format
4. Trying to delete a non-existent transaction should show an error
5. The JSON file might be corrupted

The AI adds ValueError exceptions for invalid amounts, .lower() normalization for categories, date.fromisoformat() validation for dates, and try/except blocks around JSON loading. Each error produces a clear message rather than a Python traceback.


Testing

Following the chapter's approach, Marcus asks for tests:

Write pytest tests for the BudgetTracker. Test:
1. Adding income stores a positive amount
2. Adding expense stores a negative amount
3. Balance calculation is correct after mixed transactions
4. Category summary groups correctly
5. Monthly summary filters by the correct month
6. Deleting a transaction removes it
7. Loading from empty file returns empty tracker
Use tmp_path for the data file.

All seven tests pass on the first run.


Reflection

Marcus's session followed the same workflow as Chapter 6's task manager: plan, build incrementally, evaluate, iterate, test. Three things stood out:

  1. The class-based approach was more natural for this application because the budget tracker maintains state (the loaded transactions) across operations. Marcus learned when classes add value versus when standalone functions suffice.

  2. Catching the sign convention issue early saved significant debugging time. If both positive and negative amounts had been allowed for expenses, the summary calculations would have been silently wrong --- the most dangerous kind of bug.

  3. The bar chart display was an example of the AI exceeding expectations. Marcus did not know how to implement it, but by showing an example of the desired output, the AI produced clean, working code immediately.

The complete code for the budget tracker is available in code/case-study-code.py. It includes the Transaction dataclass, the BudgetTracker class, and all features described above. Marcus uses it daily and has not opened his spreadsheet since.


Key Takeaways from This Case Study

  • Use a class when you need to maintain state across multiple operations in a session
  • Validate data at the boundary --- enforce sign conventions, normalize categories, and check date formats when data enters the system
  • Example-driven prompts shine for display formatting --- showing the AI a desired output format produces accurate results
  • Read the AI's output carefully --- Marcus caught a missing sign enforcement that would have caused subtle calculation errors
  • Apply the Chapter 6 workflow to any project --- planning, incremental building, evaluation, and testing work regardless of the application domain