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
- What designing is and how much design is enough
- Data model: entities and attributes
- Dictionary, class or list?
- The project's class diagram
- Persistence: choosing the file format
- Layered architecture
- Interface design
- Non-trivial algorithms in pseudocode
- Scheduling by sessions
- Preparing the repository and the environment
- Common mistakes and tips
- Exercises
- Conclusion
- 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.
- 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.
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.
- 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.
- 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.
- 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:
versionat 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_idstored, 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-DDstring. JSON has no date type. That format is the ISO 8601 standard, it converts withdate.fromisoformat()and, on top of that, it sorts alphabetically exactly as it sorts chronologically, which simplifies filtering and sorting.
- 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.
- 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.
- 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 ascendingTwo 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 transactionIt 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.
- 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.
- 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.pyThe .gitignore, with everything that must never enter the repository:
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:
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
categoryin 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 class — Agenda 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.
Fundamentals of Programming
Module 1: Introduction to Programming
- What is programming?
- History of programming
- Programming languages
- Development environments
- From problem to algorithm
Module 2: Core Concepts
- Variables and data types
- Operators and expressions
- Input and output
- Type conversion and data validation
Module 3: Control Structures
Module 4: Functions and Procedures
- Defining and using functions
- Parameters and return values
- Variable scope
- Breaking a program down into functions
- Functions as values: lambda and higher order
Module 5: Data Structures
- Lists and arrays
- Strings
- Dictionaries and sets
- Tuples and nested structures
- Saving data to files: text, CSV and JSON
Module 6: Basic Algorithms
Module 7: Objects and Code Organisation
- From data to objects: classes and instances
- Attributes, methods and the constructor
- Collections of objects
- Modules, packages and imports
Module 8: Good Practices and Tools
- Documentation and comments
- Debugging and error handling
- Version control
- Automated testing
- Style, readability and refactoring
