RizTech Academy logo
RizTech Academy
Capstone ProjectLesson 2 of 520 min

Planning before you code

Most people open an editor and start typing. For anything past fifty lines that costs more time than it saves, because the decisions you make in the first ten minutes shape everything after them — and the cheapest moment to change your mind is before any code exists.

This lesson is the plan. It takes about twenty minutes.

Decide the data shape first

Everything else follows from this, so it goes first.

An expense needs: an id, an amount, a category, a date, and an optional note.

{
  "next_id": 3,
  "expenses": [
    {
      "id": 1,
      "amount_paise": 25000,
      "category": "food",
      "date": "2026-09-27",
      "note": "lunch"
    },
    {
      "id": 2,
      "amount_paise": 1500000,
      "category": "rent",
      "date": "2026-09-27",
      "note": ""
    }
  ]
}

Four decisions are embedded there, each worth stating.

amount_paise, an integer. Module 2 said never store money as a float. ₹250.00 is 25000 paise. All arithmetic is exact, and we divide by 100 only when displaying. The field name says the unit so nobody has to guess — amount: 25000 would be a bug waiting to happen.

Dates as ISO strings. Module 7: JSON has no date type, "2026-09-27" sorts correctly as text, and date.fromisoformat() reads it back.

A top-level object, not a bare array. An array of expenses would be simpler today and leaves nowhere to put next_id, or a schema version, or any setting added later. An object costs one level of nesting and leaves room.

next_id stored explicitly rather than computed as "highest id plus one" — which would reuse an id after a deletion, so a deleted expense's id could come back attached to something else.

Choose the operations

From the brief, each with its input and output:

Command Takes Returns
add amount, category, note the created expense
list optional category, optional month matching expenses
summary nothing totals and a breakdown
delete id the deleted expense, or an error

Writing these down as a table is the point: it is the shape of your code before you write any.

Decide the structure

Module 9's division — data is a class, process is functions:

expense-tracker/
    src/expense_tracker/
        __init__.py
        models.py        the Expense type and its rules
        storage.py       reading and writing the JSON file
        reporting.py     filtering, totals, formatting
        cli.py           argument parsing and output
        __main__.py      entry point
    tests/
        test_models.py
        test_reporting.py
        test_storage.py
    pyproject.toml
    README.md
    .gitignore

The rule behind that split is one reason to change per file. Change the storage format and only storage.py moves. Change how a summary looks and only reporting.py does.

cli.py is the only file that prints. Everything else returns values, which is module 10's requirement for testable code — the layer that touches the outside world is thin and sits at the edge.

Decide the dependencies

argparse     standard library — command parsing
json         standard library — storage
pathlib      standard library — file location
dataclasses  standard library — the Expense type
pytest       development only
ruff         development only

Nothing to install for the program to run. That was not a goal, but module 8's question — does this dependency earn its place? — answers itself here.

Decide the failure cases

Listing these now means handling them rather than discovering them:

Situation Response
Data file missing Treat as empty; create on first save
Data file corrupt Clear message, do not overwrite it
Amount not a number Error: amount must be a number
Amount zero or negative Error: amount must be positive, got X
Unknown category Error naming the valid ones
Delete an id that does not exist Error: no expense with id X
Note extremely long Truncate at 200 characters

"Do not overwrite it" matters. If the file will not parse, the worst response is to start fresh and destroy whatever was there. Report the problem and stop; the user may be able to fix it by hand.

Decide where the data lives

Module 7's warning: a relative path resolves against wherever the program was started.

DATA_FILE = Path.home() / ".expenses.json"

The user's home directory means expenses works from anywhere. An environment variable makes it overridable and — usefully — lets tests point it somewhere disposable:

DATA_FILE = Path(os.environ.get("EXPENSES_FILE", Path.home() / ".expenses.json"))

Write the interface before the code

Sketch the function signatures. No bodies:

# models.py
@dataclass(frozen=True)
class Expense:
    id: int
    amount_paise: int
    category: str
    date: date
    note: str = ""

    @property
    def rupees(self) -> float: ...

    @classmethod
    def from_dict(cls, data: dict) -> "Expense": ...

    def to_dict(self) -> dict: ...


# storage.py
def load(path: Path) -> tuple[list[Expense], int]: ...
def save(path: Path, expenses: list[Expense], next_id: int) -> None: ...


# reporting.py
def filter_expenses(expenses, category=None, month=None) -> list[Expense]: ...
def total_paise(expenses) -> int: ...
def by_category(expenses) -> dict[str, int]: ...
def format_table(expenses) -> str: ...

Twenty minutes, and the whole program is visible. Two things usually surface here that would otherwise surface in an hour's time:

load returning a tuple of expenses and next_id is slightly awkward. Worth noticing now — the alternative is a small container class, and either is defensible. Deciding deliberately beats discovering.

frozen=True on the dataclass, from module 9. An expense does not change after creation; editing means replacing. That removes a category of bug outright.

The order to build in

Thin slices, each ending somewhere runnable:

  1. Expense with validation, and its tests
  2. storage.load and save, with a temporary hard-coded list
  3. add end to end — data survives between runs
  4. list, unfiltered
  5. list with filters
  6. summary
  7. delete
  8. Errors and messages
  9. Polish and README

Step 3 is the milestone. Once something is saved and read back, the program is real and everything after is addition rather than construction.

Check your work

Your JSON shape. If it differs from the lesson's, that is fine provided you can defend it. The three decisions worth having reached:

Amounts as integer paise, because module 2 established that money must not be a float.

Dates as ISO strings, because JSON has no date type and ISO sorts correctly as text.

A top-level object rather than a bare array, so there is somewhere to put next_id — and later a schema version, or any setting.

next_id stored rather than computed. Computing it as "highest id plus one" reuses an id after a deletion, so a deleted expense's id can come back attached to something else.

The tuple return. load() returning (expenses, next_id) is slightly awkward, and the alternative is a small container class. Either is defensible — noticing it at the planning stage rather than an hour into building is the point of writing signatures before bodies.

The failure table. At least eight rows: missing file, corrupt file, non-numeric amount, zero or negative amount, unknown category, unknown id on delete, an over-long note, and a data directory that does not exist. If you missed the last, you will meet it when save first runs on a fresh machine.

Disagreeing with a decision. All three named are genuinely arguable. Having an opinion and being able to say what the alternative would cost is the skill — "because the lesson said so" is not a reason you can use at work.

Practice

Write down, before the next lesson:

  1. Your JSON shape. If it differs from the one above, note why — there is more than one reasonable answer.
  2. Your file layout.
  3. Every function signature with a type hint and a one-line docstring, and no body.
  4. Your failure table, with at least eight rows.
  5. Your build order.

Then a harder one:

  1. Find one decision above you disagree with. Write down what you would do instead and what it would cost. next_id, storing paise, and the tuple return are all defensible either way — having an opinion and being able to justify it is the skill.

Next: building it.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship