Eleven modules after that print("Hello, World"), you have every piece on the table: a papyrus package with models, errors and tests, two ways of serving it over the web, and a data lab that answers business questions. What you have not done yet is build a whole system on your own, from a requirements sheet to a deliverable. That leap — from following lessons to deciding, implementing and verifying without a safety net — is exactly what separates someone who "took a Python course" from someone who "can build with Python". Module 12 is your self-taught final exam: Papyrus Online, the full version of Ana's bookshop. This first lesson defines the contract: what gets built, how we measure that it is right, and what deliberately stays out.

Contents

  1. Why an integrative project (and why now)
  2. What Papyrus Online is: the complete system
  3. Functional requirements (FR1-FR7)
  4. Non-functional requirements (NFR1-NFR4)
  5. Measurable acceptance criteria
  6. Scope: what stays out (and why)
  7. Two tracks: Flask API or Django site
  8. Map of requirements → course modules
  9. How to work through this module

Why an integrative project

Until now, every lesson handed you the problem and the way to solve it. In real work, nobody does that: you receive requirements (often vague ones), and the decisions — which structure, which errors, what to test — are yours. An integrative project trains three muscles no isolated lesson can train:

  • Deciding under ambiguity: choosing between valid alternatives and owning the consequences (track A or B, dict or list, JSON or CSV).
  • Integration: pieces that work on their own fail together in brand-new ways — relative paths that break, floats that don't add up, circular imports. Debugging between layers is a different skill from debugging inside a function.
  • Closure: deciding when something is "done" and shipping it. Personal projects tend to die at 80 %; this module gives you explicit acceptance criteria so that 100 % exists and is reachable.

The lessons in this module will not give you all the code. They give you structure, skeletons, contracts and checkpoints; you implement. Where a piece integrates too many things at once (like the sales service), there will be a complete, commented solution. For the rest, reference solutions: if yours is different but passes the criteria, it is every bit as valid as ours.

What Papyrus Online is

Papyrus Online is the system Ana would actually use: it manages the catalog and the members, records sales applying pricing rules and coupons, persists everything to disk, exposes it through a web interface, and produces a monthly report with numbers and a chart.

flowchart LR
    subgraph Interface
        API["Flask API (track A)<br/>or Django site (track B)"]
    end
    subgraph Core["papyrus package (domain)"]
        M["models: Book, Member"]
        S["services: sales, coupons"]
        E["errors: PapyrusError hierarchy"]
    end
    subgraph Persistence
        J["catalog.json / members.json"]
        C["sales.csv"]
    end
    subgraph Analysis["Analysis"]
        I["monthly report<br/>pandas + Matplotlib"]
    end
    API --> Core
    Core --> Persistence
    Persistence --> Analysis
    Core -. "logging → data/papyrus.log" .-> L[(papyrus.log)]

Notice the direction of the arrows: the interface uses the core, never the other way around. It is the rule M10 called "the web translates, the package solves", and in 12-02 we will turn it into architecture.

Functional requirements

Numbered so we can refer to them throughout the module (in 12-04 each one gets its own test):

  • FR1 — Catalog management (CRUD): create, read, update and delete books. Every book has a title, an author, a price and a stock. No two books may share a title (the title is the key, as it has been all course long).
  • FR2 — Members and discounts: member registration with a unique code (format NAME-NNN, like LUIS-001). An identified member gets a 5 % discount (MEMBER_DISCOUNT = 0.05) on the VAT-inclusive price. A nonexistent code must be rejected with InvalidMemberError.
  • FR3 — Sales with coupons and stock control: selling N units of a title checks the stock (if there isn't enough, InsufficientStockError and nothing is modified), applies the member rate where it applies and a coupon where it applies (PAPYRUS10 → 10 %, MEMBER5 → 5 %; unknown coupon → InvalidCouponError), decrements the stock and records the sale.
  • FR4 — Persistence: catalog and members in JSON; sales in CSV with columns date,title,units,amount. The system starts by loading the files and loses no data if a write dies halfway (atomic save, like M8's CatalogTransaction).
  • FR5 — Interface (your choice): track A, a REST API with Flask (GET/POST/PUT/DELETE /api/books, POST /api/sales, 404/409/400 errors as JSON); track B, a Django site (listing, detail, sale form with a ModelForm, admin enabled).
  • FR6 — Monthly data report: from the sales CSV, a script produces: total units and amount for the month, the top 3 titles, a weekday comparison, and a chart saved as a PNG. Run against sales_2026.csv it must reproduce the canonical numbers from M11 (520 units Jan-Jun, Saturday as the best day with 130).
  • FR7 — Logging: every sale, business error and persistence operation is recorded in data/papyrus.log at the appropriate level (INFO for operations, WARNING for business rejections, ERROR for technical failures).

Non-functional requirements

  • NFR1 — Tests: the business logic (FR1-FR4) covered with pytest. Minimum bar: the 12-04 suite (about 25 tests) all green. The acceptance criteria below are the specification for those tests.
  • NFR2 — Type hints: every public function of the papyrus package annotated (parameters and return), as you learned in 08-01.
  • NFR3 — Custom errors: no business error is communicated with a generic ValueError or with print; always the PapyrusError hierarchy from M7. The interface translates them into HTTP status codes or user messages.
  • NFR4 — README: a README.md saying what the project is, how to install it (venv + requirements.txt), how to use it and how to run the tests. Full template in 12-05.

Measurable acceptance criteria

"It works" is not a criterion; this is. Every row can be verified with a concrete action:

Requirement Acceptance criterion (verifiable)
FR1 Adding "Faust" twice produces an error (409 in the API / a message in the form); after deleting a book, looking it up returns BookNotFoundError (404).
FR2 Selling The Odyssey to LUIS-001 charges 12.35 EUR; to Julia (not a member), 13.00 EUR; with code JOHN-999, InvalidMemberError.
FR3 Selling 5 copies of The Odyssey with a stock of 4 raises InsufficientStockError and the stock is still 4; Faust + member + PAPYRUS10 charges 18.67 EUR; coupon NOSUCHInvalidCouponError.
FR4 After selling and restarting the program, the stock read from catalog.json reflects the sale; killing the process mid-save does not leave a corrupt JSON.
FR5 A: all 6 endpoints respond with the right status codes (checked with the test client). B: listing, detail, sale and admin all working (checked with Client).
FR6 Running the report over sales_2026.csv prints 520 total units and Saturday as the best day (130 u), and leaves report_2026.png on disk.
FR7 After a sale rejected for lack of stock, data/papyrus.log contains a WARNING line with the title and the units requested.
NFR1-4 pytest all green; mypy (or a manual review) finds no unannotated public functions; a search for unjustified generic except Exception returns zero; the README lets someone else start the project without asking you anything.

The amounts come from the course-long formula: price × 1.04 (VAT) × 0.95 (member), rounded to 2 decimals, with the coupon applied on top of that result. Check it against the four canonical books: The Odyssey 12.50 → 13.00 / 12.35; Hamlet 9.95 → 10.35 / 9.83; Don Quixote 15.90 → 16.54 / 15.71; Faust 21.00 → 21.84 / 20.75.

Scope: what stays out

Just as important as the what is the what not. Deciding scope is engineering; wanting to do everything is the surest way to finish nothing.

Out of scope Why Where to pick it up
Real authentication (users, passwords, sessions) Requires serious security (hashing, CSRF, tokens); done badly it is worse than absent Extensions in 12-05
Payments Integration with external gateways, beyond the syllabus Extensions in 12-05
Production deployment Servers, HTTPS, domains: a different trade Extensions in 12-05
A SQL database in track A JSON/CSV are enough for Papyrus's volume and are what M6 practiced Extensions in 12-05
An elaborate front end (JavaScript, advanced CSS) This is a Python course; minimal HTML is enough Extensions in 12-05

If you finish and want more, perfect: 12-05 ranks these extensions by difficulty. But first you ship the agreed scope.

Two tracks

FR5 is satisfied by one of two paths. Choose based on what you want to strengthen, not on which one "sounds better":

Track A — Flask API Track B — Django site
What you build A JSON REST API on top of your papyrus package A web application with templates, forms and admin
Course foundation 10-02 and 10-03 10-04 and 10-05
Persistence Your own JSON/CSV repositories (FR4 in full) Django's ORM for the catalog; sales stay in CSV
Relative effort Less on the interface, more on persistence (all of it is yours) More on the interface (templates, forms), less on persistence (the ORM gives you a lot for free)
Strengthens API design, HTTP status codes, layer separation Full frameworks, conventions, ORMs
Typical risk Forgetting to validate the JSON input Putting business logic in the views
Choose it if… You lean towards pure backend or data work You lean towards product web development

Both tracks share 70 % of the project (the whole core, sales persistence, the report and the business tests). The decision affects only milestone H4 of the plan.

Map of requirements → course modules

Nothing in this project is new. Every requirement points to modules where you already learned it — this table is your cheat sheet for "where to go back and review":

Requirement What it needs Module(s) where you learned it
FR1 catalog dataclass Book, dictionaries as an index, warehouse functions M5 (05-06), M4 (04-03), M3
FR2 members Validation, InvalidMemberError, constants M2, M7 (07-04), M1
FR3 sales and coupons Conditionals, custom exceptions, apply_coupon, rounding M2, M7, M3, M1 (floats)
FR4 persistence JSON, CSV, pathlib, atomic save with a context manager M6, M8 (08-04)
FR5-A Flask API Routes, JSON, status codes, errorhandler M10 (10-02, 10-03)
FR5-B Django site Models, views, templates, ModelForm, admin M10 (10-04, 10-05)
FR6 report pandas (groupby), Matplotlib (savefig) M11 (11-03, 11-04)
FR7 logging logging with levels and a file M7 (07-05)
NFR1 tests pytest, fixtures, parametrize, tmp_path M9 (09-01 to 09-03)
NFR2 type hints Annotations as specification M8 (08-01)
NFR3 errors The PapyrusError hierarchy M7 (07-04)
NFR4 README Virtual environments, pip, requirements M1 (01-06)

And what about M11 as a whole, regression included? It comes back in 12-05: the final report will include a recommendation in the style of the Sant Jordi order — Sant Jordi being Catalonia's Book Day — (prediction 172, Ana's order 190: the model recommends, the person decides).

How to work through this module

The five lessons follow the real life cycle of a project: requirements (this one), design (12-02), construction (12-03), verification (12-04) and delivery (12-05). Work with your editor open: every lesson ends with milestones you must complete before the next one. Reuse your code from earlier modules without shame — reusing your own work is what professionals do — but copy it into a new project and clean it as you bring it over: the final project deserves to start in a clean folder.

Common Mistakes and Tips

  • Starting to code today. The temptation is enormous and the mistake is a classic: without the 12-02 design you will end up rewriting. One day of design saves three of refactoring.
  • Widening the scope halfway through ("while I'm at it, I'll add a login…"). It's called scope creep and it kills more personal projects than any bug. Jot the idea down for 12-05 and move on.
  • Choosing track B "because Django is more professional". Neither is more professional; they are different trades. Choose by what you want to strengthen.
  • Treating the acceptance criteria as decoration. They are the contract: in 12-04 they become tests, one by one. If a criterion seems ambiguous now, rewrite it now — that is far cheaper than discovering the ambiguity with the code already written.
  • Skipping the canonical amounts. 12.35, 9.83, 15.71, 20.75 have been the truth all course long. If your system produces a different number, the bug is yours, not the table's.

Exercises

This module replaces practice exercises with project milestones: real work on your deliverable, with verification criteria.

  1. Milestone 0.1 — Choose your track and put it in writing. Create the project folder (papyrus_online/) with a DECISIONS.md file and write 3-5 lines: which track you choose and why, in terms of what you want to strengthen. This file will grow throughout the module.
  2. Milestone 0.2 — Audit of reusable pieces. Go through your M1-M11 code and list in DECISIONS.md which files you will reuse (with their origin) and which you will build from scratch. At a minimum: review models.py, errors.py, coupons.py, warehouse.py, your M9 tests and your M10 app.
  3. Milestone 0.3 — One more criterion. Write an additional acceptance criterion for the requirement you understand least, in the format of the table (concrete action → verifiable result). Writing it will force you to understand the requirement.

Solutions

Reference answers — what matters is that yours exist and are concrete:

  1. A valid example: "Track A. In M10, Django felt more guided and Flask left me with more doubts about HTTP status codes and input validation; I want to close those doubts. Track A also forces me to build the persistence in full, which is where I feel least confident."
  2. Typical reusables: models.py (M5, adding Member), errors.py (M7, as is), coupons.py (M7, as is), apply_coupon and the M1 constants; from scratch or heavily reworked: warehouse.py (it becomes services + repositories in 12-02), the interface (rebuilt on the new design), the report (a new script on top of what M11 taught).
  3. An example for FR4: "I run a sale, kill the process, start again: the catalog loads without errors and the stock is consistent (either the whole sale was applied, or none of it — never halfway)." If your criterion doesn't say how to verify it, it isn't a criterion: it's a wish.

Conclusion

You now have the contract: seven functional requirements, four non-functional ones, acceptance criteria you can actually check (and which in 12-04 will become tests), a scope with clear borders and two tracks you have already chosen between. You have also seen that there is nothing new to learn: every requirement points to a module where you already lived the solution. What is new is the assembly — and assembly starts with the blueprint. In the next lesson we will turn these requirements into a plan: milestones in a deliberate order, a layered architecture, data schemas and function contracts written before any code. Resist the pull of the editor one more day: first the blueprint, then the bricks.

Python Programming Course

Module 1: Introduction to Python

Module 2: Control Structures

Module 3: Functions and Modules

Module 4: Data Structures

Module 5: Object-Oriented Programming

Module 6: File Handling

Module 7: Error and Exception Handling

Module 8: Advanced Topics

Module 9: Testing and Debugging

Module 10: Web Development with Python

Module 11: Data Science with Python

Module 12: Final Project

© Copyright 2026. All rights reserved