In Project definition the what was settled: a one-page document with the problem, the scope in MoSCoW boxes, the numbered requirements and their acceptance criteria. Now comes the how, and it still does not involve typing code. Designing means taking the expensive decisions up front: which entities exist and whether they are classes or dictionaries, what format the data is stored in, how the program is split into modules, what the user sees on screen and which algorithms are needed. Each of those decisions, taken on paper, costs ten minutes; taken halfway through the implementation it costs an afternoon of rewriting. By the end of the lesson you will also have the work plan: the list of small, estimated, ordered tasks that turns "do a project" into "do the next task".

Contents

  1. What designing is and how much design is enough
  2. Data model: entities and attributes
  3. Dictionary, class or list?
  4. The project's class diagram
  5. Persistence: choosing the file format
  6. Layered architecture
  7. Interface design
  8. Non-trivial algorithms in pseudocode
  9. Scheduling by sessions
  10. Preparing the repository and the environment
  11. Common mistakes and tips
  12. Exercises
  13. Conclusion

  1. What designing is and how much design is enough

Designing means deciding the shape of the program before building it: its pieces, their responsibilities and how they talk to each other. The reasonable question is how much. The answer, for a project of this size, is: just enough to start coding without hesitation in the first hour, and no more.

A sufficient design for your final project fits on three or four sheets of paper and contains six things:

Design piece Question it answers Format
Data model What things exist and what do I store about each? Table of entities and attributes
Chosen structure Class, dictionary or list for each entity? A one-line justified decision
Persistence Where and how are they stored? File format with a real example
Architecture Which modules are there and what does each depend on? Dependency diagram
Interface What does the user see and choose? Menu map and screen sketches
Algorithms Which parts are not obvious? Pseudocode with a desk check

What is not design at this level: writing every function in advance, deciding the name of every variable or drawing twenty-box diagrams. That is called analysis paralysis and it eats the project's time. If you are torn between two options and neither is clearly worse, pick the simpler one and move on: in module 8 you learned to refactor, and refactoring exists precisely because no initial design is perfect.

  1. Data model: entities and attributes

An entity is an important noun in your domain: something you keep several instances of and want to know things about. The fastest way to find them is to underline the nouns in the requirements from 09-01. In MyExpenses, the FRs talk about expense, income, amount, category, date, description and month.

From that list you have to separate the wheat from the chaff with two questions:

  • Do I store many instances of this? "Expense" yes; "month" no, it is just a filtering criterion.
  • Does it have attributes of its own beyond its name? "Category" in MyExpenses only has a name, so it does not need to be an entity: a string inside the transaction is enough.

That second dismissal is what simplifies a first project the most. Applied to MyExpenses, a single real entity is left:

Entity Attribute Type Required Rules
Transaction id int Yes Automatic, unique, consecutive
date date Yes Not in the future; blank = today
description str Yes 1 to 60 characters, no surplus spaces
amount float Yes Not 0; negative = expense, positive = income
category str Yes Lower case; one of the known categories

Notice the decision in the last row of amount: instead of a kind field with the values "expense" or "income", the sign of the amount encodes the kind. It is a design decision with good consequences — the month's balance is a plain sum — and one bad consequence: you have to remember it and document it, because it is not obvious. That kind of decision is exactly what you will be asked about in Project presentation, so write it down now along with its reason.

Besides the entities, define the domain constants: in MyExpenses, the allowed categories.

CATEGORIES = ("food", "transport", "leisure", "home", "health", "income", "other")

It is a tuple and not a list because it does not change while the program runs (05-04), and it lives in a single place so that adding a category means editing one line, not searching the whole codebase.

  1. Dictionary, class or list?

With the entities identified you have to choose how to represent them in Python, applying the decision table from Tuples and nested structures and what you learned in From data to objects:

Use... When... In MyExpenses
List There is just a sequence of values of the same type, with no names The amounts for a month, to add them up
Tuple A small, fixed group of values that does not change CATEGORIES; the (category, total) pair in a report
Dictionary Named fields, a structure that changes, or data coming from JSON The result of grouping by category: {"food": -212.4, ...}
Class The data has behaviour: it validates, formats and compares itself Transaction

The rule that settles almost every doubt is this one: if on top of storing data you are going to write functions that always operate on that data, it is a class. In MyExpenses, a transaction validates itself on creation (amount other than zero, known category, date not in the future), is displayed formatted as a table row and is compared by date for sorting. Three behaviours: a class, without a doubt.

And there is a second class that is not a domain entity but is very much a design piece, exactly like Agenda in EasyTask: the collection with logic. Book is an entity; Library is the class that knows how to search, filter and summarise. In MyExpenses it is called Ledger.

  1. The project's class diagram

classDiagram
    class Transaction {
        +int id
        +date date
        +str description
        +float amount
        +str category
        +is_expense() bool
        +to_dict() dict
        +from_dict(d) Transaction
        +__str__() str
    }
    class Ledger {
        -list transactions
        -int next_id
        +add(tx) Transaction
        +remove(id) bool
        +find(id) Transaction
        +for_month(yyyy_mm) list
        +total_by_category(yyyy_mm) dict
        +balance(yyyy_mm) float
        +__len__() int
        +__iter__()
    }
    class InvalidTransaction {
        <<exception>>
    }
    Ledger "1" o-- "*" Transaction : contains
    Transaction ..> InvalidTransaction : raises

The diagram says three things at a glance. First, that Ledger contains transactions: that is composition (07-03), the same relationship Agenda had with Task. Second, that validation lives in Transaction and raises its own InvalidTransaction exception, just like InvalidTask in EasyTask: the data protects itself and no other layer can create an invalid transaction. And third, something you can see from what does not appear: neither Transaction nor Ledger knows anything about files or print. That absence is the architecture of section 6 drawn by omission.

Draw your diagram with two or three classes at most. If you end up with six, almost certainly several of them are attributes dressed up as classes.

  1. Persistence: choosing the file format

Picking up Saving data to files, you have three realistic options:

Format Works well when Works badly when Cost
Plain text The data is loose lines with no structure (notes, a log) There are fields to separate; commas or line breaks turn up Minimal, but you have to invent the format
CSV Flat, uniform records that somebody will open in a spreadsheet There are lists inside a field or nested structures Low; csv from the standard library
JSON There is nesting, optional fields or mixed types The file has to open in Excel without conversion Low; json from the standard library

For MyExpenses: JSON as the main format, CSV as an export. The reasoning is what you must be able to defend: JSON preserves types (a float comes back as a float, not as the string "12.5"), it can grow with optional fields without breaking old files, and json.load validates the structure for you. CSV only comes in where it has a real advantage — FR-7, opening the month in a spreadsheet — and as a one-way output: it is exported, not imported, which saves all the work of rebuilding types from text.

And now the important bit: write down the concrete format with a real example before programming anything. This is expenses.json:

{
  "version": 1,
  "next_id": 4,
  "transactions": [
    {"id": 1, "date": "2026-08-01", "description": "Weekly shop", "amount": -62.35, "category": "food"},
    {"id": 2, "date": "2026-08-01", "description": "Travel pass", "amount": -32.0, "category": "transport"},
    {"id": 3, "date": "2026-08-03", "description": "August salary", "amount": 1450.0, "category": "income"}
  ]
}

Three deliberate details, and all three are justified:

  • version at the top. Today it does nothing; the day you change the format, your code will be able to read the old files instead of breaking. It costs one line.
  • next_id stored, not recalculated. If you worked it out as "the highest id plus one", deleting the last transaction would reuse its id, and a reused id is a source of confusion.
  • Dates as a YYYY-MM-DD string. JSON has no date type. That format is the ISO 8601 standard, it converts with date.fromisoformat() and, on top of that, it sorts alphabetically exactly as it sorts chronologically, which simplifies filtering and sorting.

  1. Layered architecture

A program is split into layers for the reason you already know from Breaking a program down into functions and Modules, packages and imports: each piece has a single reason to change. If the total-by-category calculation were mixed in with the print calls, you could not test it without simulating the screen, nor change the presentation without risking a change to the calculation.

The four layers and their file:

Layer File Responsibility What it is forbidden
Model model.py Entities and their validation rules print, input, files
Logic ledger.py The collection and its operations: add, find, filter, aggregate totals print, input, files
Storage storage.py Reading and writing JSON and exporting CSV print, input
Interface interface.py Menu, input, print, screen formatting Domain calculations
flowchart TD
    M["__main__.py<br/>startup"] --> I["interface.py<br/>menu and screens"]
    I --> C["ledger.py<br/>collection logic"]
    I --> A["storage.py<br/>JSON and CSV"]
    A --> C
    C --> MO["model.py<br/>Transaction and validation"]
    A --> MO

The arrows always point downwards: the interface knows the logic, the logic knows the model, and the model knows nobody. No arrow points up. That rule, which looks trivial, is what lets you write tests for the model and the logic without touching the screen, and it is the same structure you have already seen working in EasyTask (model.py, agenda.py, storage.py, interface.py, __main__.py).

Add __init__.py so that the directory is a package and __main__.py so that you can run python -m myexpenses, exactly as was done in 07-04.

  1. Interface design

A badly designed menu shows in the demo. Draw the map before programming it:

flowchart TD
    Menu["MAIN MENU"] --> O1["1 Record expense"]
    Menu --> O2["2 Record income"]
    Menu --> O3["3 Month transactions"]
    Menu --> O4["4 Summary by category"]
    Menu --> O5["5 Delete transaction"]
    Menu --> O6["6 Export month to CSV"]
    Menu --> O0["0 Exit"]
    O1 --> Menu
    O2 --> Menu
    O3 --> Menu
    O4 --> Menu
    O5 --> Conf["Confirm y/n"] --> Menu
    O6 --> Menu
    O0 --> Fin["Save and finish"]

Three design rules can be read off that map: every path returns to the menu (never leave the user somewhere with no way out), a single screen deep (no submenus in a first project) and destructive actions ask for confirmation. Option 0 for exiting is a convention: 0 is always in the same place even as the list grows.

Then sketch each screen in text, exactly as you want to see it. This is not cosmetics: writing the sketch reveals which data you need to calculate.

=========================================
  SUMMARY FOR 2026-08                (4)
=========================================
  food            -212.40 EUR   38.1 %
  transport       -132.00 EUR   23.7 %
  home             -98.50 EUR   17.7 %
  leisure          -57.20 EUR   10.3 %
  other            -57.00 EUR   10.2 %
-----------------------------------------
  Expenses        -557.10 EUR
  Income         +1450.00 EUR
  BALANCE         +892.90 EUR
=========================================

That sketch has just generated three calculation requirements that were not explicit: you need the percentage of total expenses, you need to sort from largest to smallest expense and you need to separate expenses from income in the footer. Drawing the screen is the cheapest way to find hidden work.

  1. Non-trivial algorithms in pseudocode

Most of your project is obvious code. But there are always two or three points where it is not, and those — only those — get written out beforehand in pseudocode, with the technique from From problem to algorithm. In MyExpenses there are two.

Algorithm 1: total by category for a month.

FUNCTION total_by_category(transactions, month):
    totals <- empty dictionary
    FOR EACH t IN transactions:
        IF t.date starts with month THEN
            totals[t.category] <- totals.get(t.category, 0) + t.amount
    RETURN totals sorted by value ascending

Two specific decisions there. Comparing by string prefix ("2026-08-03" starts with "2026-08") works because the ISO format allows it, the decision from section 5 paying dividends. And get(key, 0) is the dictionary accumulation pattern from Dictionaries and sets, which avoids the if key not in totals. Ascending order by value puts the most negative amounts first, that is, the biggest expenses, which is what the sketch in section 7 asks for.

Desk check with four transactions and month = "2026-08":

Step Transaction Match? totals afterwards
1 2026-07-30, -20.00, food No {}
2 2026-08-01, -62.35, food Yes {food: -62.35}
3 2026-08-01, -32.00, transport Yes {food: -62.35, transport: -32.00}
4 2026-08-04, -15.00, food Yes {food: -77.35, transport: -32.00}

Sorted result: [("food", -77.35), ("transport", -32.00)]. Correct: the July expense is left out and the two food purchases accumulate under the same key. This table, done in three minutes on paper, is what stops you from discovering in the demo that the month filter was also picking up the wrong year.

Algorithm 2: assigning the next identifier.

FUNCTION add(ledger, transaction):
    transaction.id <- ledger.next_id
    ledger.next_id <- ledger.next_id + 1
    append transaction to the list
    RETURN transaction

It looks too simple to be worth writing, and that is precisely why it is worth it: written out like this you can see that the counter must be persisted with the data (section 5) and that there is an edge case, the first startup with no file, where next_id is 1.

  1. Scheduling by sessions

A 15-to-25-hour project is not planned "by weeks": it is split into one-to-three-hour tasks, each with a visible result. If a task does not fit into three hours, split it; if you cannot estimate it, you do not understand it yet and it needs a bit more design.

# Task Est. Depends on Visible result
T1 Repository, environment, skeleton that starts and exits 1 h python -m myexpenses shows the menu and exits
T2 Transaction class with validation and InvalidTransaction 2 h T1 A transaction can be created from the interpreter
T3 tests/test_model.py 1 h T2 pytest green
T4 Ledger class: add, find, remove, __len__ 2 h T2 Adding and deleting from the interpreter
T5 storage.py: save and load JSON 2 h T2 The file is created and read back
T6 tests/test_storage.py with tmp_path 1 h T5 Save/load cycle tested
T7 Interface: menu, record expense (FR-1) end to end 3 h T4, T5 First complete feature
T8 List a month's transactions (FR-3) 2 h T7 Listing screen
T9 total_by_category and balance + tests (FR-4) 3 h T4, T3 Summary screen
T10 Record income (FR-2) and delete (FR-5) 2 h T7 Complete menu of the Musts
T11 Export to CSV (FR-7) 2 h T8 A file opened in the spreadsheet
T12 Robustness: try/except, logging, input review 2 h T10 No input breaks the program
T13 README, docstrings, black and ruff, v1.0 tag 2 h T12 Presentable repository

Estimated total: 25 hours. And now the rule almost nobody applies the first time: multiply your estimate by 1.5. Not because you are slow, but because every beginner's estimate leaves out the time spent hunting for bugs. If the result does not fit into the time you have, do not speed up: drop a Should.

flowchart LR
    H1["Milestone 1<br/>Starts and exits<br/>T1"] --> H2["Milestone 2<br/>Model tested<br/>T2 T3"]
    H2 --> H3["Milestone 3<br/>Saves and loads<br/>T4 T5 T6"]
    H3 --> H4["Milestone 4<br/>First full FR<br/>T7"]
    H4 --> H5["Milestone 5<br/>All the Musts<br/>T8 T9 T10"]
    H5 --> H6["Milestone 6<br/>Presentable v1.0<br/>T11 T12 T13"]

The principle that orders all of this: the skeleton that starts has to exist on day one. A python -m myexpenses that shows the menu and exits, even if the options do nothing, solves the environment, the package structure and how it runs all at once, and those are exactly the problems that block beginners. From there on the program is never broken again: every task leaves it runnable.

  1. Preparing the repository and the environment

Before the first line of code, get the ground ready. This is task T1 and it takes fifteen minutes, picking up Development environments and Version control:

mkdir myexpenses-project && cd myexpenses-project
python -m venv .venv
source .venv/bin/activate        # on Windows: .venv\Scripts\activate
pip install pytest black ruff
pip freeze > requirements-dev.txt

git init
mkdir myexpenses tests
touch myexpenses/__init__.py myexpenses/__main__.py myexpenses/model.py
touch myexpenses/ledger.py myexpenses/storage.py myexpenses/interface.py

The .gitignore, with everything that must never enter the repository:

.venv/
__pycache__/
*.pyc
expenses.json
*.log
.pytest_cache/

Note expenses.json: personal data does not get versioned. What goes in the repository is sample_data.json with made-up transactions, and the real working file stays out. It is the same criterion by which easytask.log was excluded in EasyTask.

And the first commit, which already puts the design on record:

git add .
git commit -m "Initial project structure and definition document"

Common Mistakes and Tips

  • Over-designing. Fifteen-class diagrams, inheritance hierarchies just in case, a layer of abstraction over the file "in case one day it is a database". That day will not come in this project. Design for what is in the Must box.
  • Turning everything that moves into a class. If an entity only has a name and no behaviour — like category in MyExpenses — it is a string. A single-attribute class is almost always surplus.
  • Choosing the data format out of habit. CSV looks simpler until you need to store a list inside a field or recover a number that came back as a string. Choose with the table in section 5, not by inertia.
  • Skipping the screen sketches. It is the step that uncovers the most hidden work: in MyExpenses it turned up three calculations no requirement mentioned.
  • Planning in big tasks. "Do the interface" is not a task, it is a month of feeling like you are getting nowhere. A task is "record an expense end to end": three hours and a result you can see.
  • Tip: write down the reason for every decision. One line per decision in the DEFINITION.md ("JSON because it preserves types and accepts new fields"). In 09-04 you will have to defend them and you will not remember.
  • Tip: if you are torn between two designs, pick the one that is easier to undo. It is almost always the simpler one, and you already know how to refactor (08-05).

Exercises

These continue the project you defined in 09-01. Save the results in a DESIGN.md next to the DEFINITION.md.

Exercise 1: Data model and class diagram

Underline the nouns in your requirements and build the entity table with their attributes, types, whether they are required and their validation rules. Justify in one line per entity whether it will be a class, a dictionary or a list. Draw the class diagram in mermaid, including the collection class and your own exception. Three classes maximum.

Exercise 2: Persistence, layers and interface

Choose the file format with the table in section 5 and write a real example of your data file with three records. Split the code across the modules of the four layers and draw the dependency diagram. Draw the menu map and sketch in text the two most important screens of your program.

Exercise 3: Algorithms and work plan

Identify the two or three non-trivial algorithms in your project, write them in pseudocode and do a desk check of at least one of them with four or five input values. Then build the table of one-to-three-hour tasks with estimates and dependencies, multiply the total by 1.5 and check whether it fits into the time you have. If it does not, state which Should you are dropping.

Solutions

Solution 1. Sections 2, 3 and 4 are the solution for MyExpenses: a single Transaction entity (a class, because it validates, formats and compares), the category reduced to a string, and Ledger as the collection with logic. Dismissing Category as a class is the interesting point: it only had a name. If your project needed a budget per category, the decision would flip, because then it would indeed have attributes and behaviour of its own. Rubric: do you have three classes or fewer? Does each class have at least two behaviours that justify being one? Does each attribute have a type and a validation rule? Is there a custom exception for invalid data?

Solution 2. Sections 5, 6 and 7. The criteria you must be able to defend: JSON because there are numeric types and the format may grow; CSV only as a one-way export; the layers kept separate so the logic can be tested without a screen; the single-depth menu with confirmation on deletion. Rubric: is the file example genuinely valid JSON or CSV, with three real records? Do all the arrows in the dependency diagram point downwards, with no cycles? Did your screen sketch uncover a calculation you had not anticipated? If it uncovered none, look again: there is almost always one.

Solution 3. Sections 8 and 9. Notice that the thirteen tasks in MyExpenses follow the order model → tests → logic → storage → interface → extras, with T1 (the skeleton that starts) first and T7 marking the milestone of the first complete feature. That is the build order developed in Implementation and testing. Rubric: is every task under three hours? Does each task have a visible, verifiable result? Is there a one-hour T1 task that leaves the program starting up? Have you applied the 1.5 factor? Is your pseudocode independent of Python, that is, could you translate it into another language?

Conclusion

Designing means taking the expensive decisions up front, and for a project of this size it fits on three or four sheets of paper. The data model comes from underlining the nouns in the requirements and discarding those with neither multiple instances nor attributes of their own; each entity is represented as a list, tuple, dictionary or class according to the table from 05-04 and 07-01, with the rule that anything which has behaviour on top of data is a class. To the entities you add the collection classAgenda in EasyTask, Ledger in MyExpenses — which concentrates the logic for searching, filtering and summarising, and a custom exception for invalid data.

Persistence is chosen on merit: plain text for unstructured lines, CSV for flat records that a spreadsheet will see, JSON when there are types, nesting or fields that will grow; and the concrete format is written out with a real example before programming, with details that save hours later — a version number, a persisted identifier counter and ISO YYYY-MM-DD dates, which sort alphabetically exactly as they sort in time. The layered architecture splits the code into model, logic, storage and interface, with dependencies always pointing downwards and the model knowing nobody, which is what makes testing without a screen possible. The interface is designed with a single-depth menu map where every path returns to the start and destructive actions ask for confirmation, plus a text sketch of each screen that almost always uncovers hidden calculations. The two or three non-trivial algorithms are written in pseudocode and validated with a desk check before being translated. And the work plan splits everything into one-to-three-hour tasks with a visible result, ordered by dependency, estimated and multiplied by 1.5, with the skeleton that starts as the very first task and the repository and environment ready before a single line is written.

With the what and the how settled, nothing stands in the way of starting. In Implementation and testing we will build for real: the incremental strategy that keeps the program always runnable, the layer-by-layer build order with the tests written alongside the code, what to do when something fails and you do not know why, and the checklist that decides when the implementation is finished.

© Copyright 2026. All rights reserved