Quality and handoff
- Lesson 18
- 50 min
- Module 6: From notebook to maintainable project
Lesson 18 of 18 · Python for Practical AI EngineeringQuality and handoff
1. Computer and environment basics
2. Values and control flow
3. Functions and program structure
4. Files, data and external services
5. Debugging and reliability
6. From notebook to maintainable project
What you will be able to do
Hand ticket-cleaner to another person. They get a folder and follow the README. In five minutes, the program runs and the tests pass on their computer, without a question to you. This is the test of every project that you build after this course.
Before you startWhat to know and to have ready
What you need to know
- The lock file, and how to make an environment again, from Isolate dependencies.
- The command
python -m ticket_cleaner, from Command-line interface.
What you need on your computer
python -m ticket_cleaner data/tickets.csvworks, andpython -m pytestpasses. You made the command in Command-line interface: Guided practice and its tests in Test the command. If either fails, do those sections again.requirements.txtwith httpx and pytest, from Focused tests: Install pytest, andrequirements-lock.txt, from Isolate dependencies: Record the versions. This lesson makes the lock file again..env.example, from Configuration and secrets: Document the settings.- The practice files that this lesson removes, if you have them. They are
first_run.py(Install and verify Python),ticket_values.py(Basic values),urgent_report.py(Modules and imports),try_mock.py(Basic HTTP client use),try_token.py(Configuration and secrets) andshow_args.py(Command-line interface). - Git is optional. With Git, the fresh-copy test uses
git clone, from the Git course. - A terminal in
~/projects/ticket-cleaner. You made the folder in Files and terminal: Guided practice. - The project’s environment
.venv, active in your terminal (the prompt starts with(.venv)). You made it in Isolate dependencies: Guided practice. If it is missing or broken, delete it and make it again from the lock file, as in Make the environment again from nothing. - An internet connection, to install ruff (about 12 MB).
Tested with Python 3.14.6, ruff 0.16.10 and pytest 9.1.1 on macOS, and Python 3.14.4 on Ubuntu 26.04.
Formatting and linting
Section titled “Formatting and linting”Code is read many more times than it is written. Two tools make it easier to read and find some bugs before you run it:
- A formatter rewrites the layout of your code to one standard style: spaces, line length, quotes, empty lines. It does not change what the code does. Nobody spends time on layout in reviews, and every file looks the same.
- A linter reads your code, and reports patterns that are often bugs. Examples are an import that you do not use, a name that does not exist, and a list as a default value.
ruff is one tool that does both, and it is fast. Add it to the project as a development dependency:
-
Add
ruff==0.16.10torequirements.txt. Add comments that separate what the program needs from what you need to work on it:requirements.txt # Needed to run the programhttpx==0.28.1# Needed to work on the program: tests, formatting and lintingpytest==9.1.1ruff==0.16.10 -
Install it:
python -m pip install -r requirements.txt. -
Make
pyproject.tomlin the project folder. Python tools read their settings from this file:pyproject.toml # Settings for the development tools. The program itself needs only# requirements-lock.txt.[tool.ruff]line-length = 88[tool.ruff.lint]# The default rules, plus import sorting (I) and common bug patterns (B).extend-select = ["I", "B"][tool.pytest.ini_options]testpaths = ["tests"]testpathstells pytest to look for tests only intests/.
Worked example: what ruff finds
Section titled “Worked example: what ruff finds”Amira makes a file with some typical problems, ticket_cleaner/messy.py, to see what ruff does:
from ticket_cleaner.records import Ticketimport json
def add_problem(problem, problems=[]): problems.append(problem) return problems
def count_open(tickets:list[Ticket])->int: return len( [t for t in tickets if t.status=='open'] )She runs the linter on it:
ruff check ticket_cleaner/messy.pyI001 [*] Import block is un-sorted or un-formatted --> ticket_cleaner/messy.py:1:1
F401 [*] `json` imported but unused --> ticket_cleaner/messy.py:2:8
B006 Do not use mutable data structures for argument defaults --> ticket_cleaner/messy.py:5:35 |5 | def add_problem(problem, problems=[]): | ^^help: Replace with `None`; initialize within function
Found 3 errors.[*] 2 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).Each finding has a code, a message and a place (file:line:column). B006 is the default list that remembers, from Function contracts: the linter finds it without running the code. The [*] findings are safe to fix automatically:
ruff check --fix ticket_cleaner/messy.pyThis removes the unused import and sorts the imports. B006 remains, because the fix changes what the function does, so a person must decide. Then the formatter fixes the layout:
ruff format ticket_cleaner/messy.pyfrom ticket_cleaner.records import Ticket
def add_problem(problem, problems=[]): problems.append(problem) return problems
def count_open(tickets: list[Ticket]) -> int: return len([t for t in tickets if t.status == "open"])Two empty lines between functions, spaces around :, -> and ==, double quotes, no spaces inside brackets. The code does the same thing, and now looks like all other Python code. She fixes B006 by hand, as in Module 3, then deletes messy.py, because it was only an example.
A clean project layout
Section titled “A clean project layout”During the course, you made some files to practise: first_run.py, ticket_values.py, urgent_report.py, try_mock.py, try_token.py, show_args.py. They are not part of the program. A new person cannot tell which files matter, so remove them (or move them to a folder outside the project).
The project then has a clear layout:
ticket-cleaner/├── .env.example the settings, without secret values├── .gitignore├── README.md how to set up, run and work on it├── pyproject.toml settings for ruff and pytest├── requirements.txt what the project uses directly├── requirements-lock.txt the exact version of every package├── data/ sample data├── tests/ one test file for each module└── ticket_cleaner/ the packageSome files must not be in the copy that you hand over, because the other person makes them again, or because they are private. List them in .gitignore, which Git and many other tools respect:
.venv/__pycache__/.pytest_cache/.ruff_cache/reports/*.tmp.envUpdate .env.example: TICKETS_SOURCE is now the input argument, so remove it, and add LOG_LEVEL.
The README
Section titled “The README”The README is the first file that a person reads. For a project that someone must run, it answers these questions, in this order:
| Section | Answers |
|---|---|
| First line | What does it do, in one or two sentences? |
| Requirements | What must I have before I start? |
| Set up | Exactly which commands do I run, once? |
| Run | Which command runs it, and what do I see when it works? |
| Settings and options | What can I change? |
| Exit codes | How does a script that runs it know the result? |
| Work on the code | How do I run the tests and the checks? |
Write the commands exactly as a person types them, and show the expected output, so they know that each step worked. These are the same rules as for a lesson. Here is the start of Amira’s README. The full file has every section from the table:
# ticket-cleaner
Checks support-ticket records from a CSV file, a JSON file or a web service, and writes a summary report. Every record that is not valid is listed in the report, with the reason.
## Requirements
- Python 3.12 or newer. Check with `python3 --version` (`python --version` on Windows).- No account. The `remote` input downloads a public file.
## Set up
Do these steps once, in a terminal in this folder.
1. Make a virtual environment: `python3 -m venv .venv`. On Windows, type `python` instead of `python3`.2. Activate it. On macOS and Linux: `source .venv/bin/activate`. On Windows (PowerShell): `.venv\Scripts\Activate.ps1`. The prompt now starts with `(.venv)`.3. Install the exact versions of the dependencies: `python -m pip install -r requirements-lock.txt`
## Run
```shpython -m ticket_cleaner data/tickets.csv```
Expected output (the last lines):
```text15 rows: 9 valid, 6 rejected.Report: reports/summary.json```Guided practice: prepare the handoff
Section titled “Guided practice: prepare the handoff”-
Add ruff and
pyproject.tomlas above. -
Run the checks on the whole project:
Terminal window ruff check .ruff format .Fix what
ruff checkreports. Run both again, untilruff check .showsAll checks passed!andruff format .reports no files to change. -
Run the tests again:
python -m pytest. The formatter must not have changed what the code does, and the tests prove it. -
Remove the practice files. Make
.gitignore, and update.env.example. -
Make the lock file again in a new environment, so it has only the packages in
requirements.txt(see the common mistake below):Terminal window deactivaterm -rf .venvpython3 -m venv .venvsource .venv/bin/activatepython -m pip install -r requirements.txtpython -m pip freeze > requirements-lock.txtTerminal window deactivateRemove-Item -Recurse -Force .venvpython -m venv .venv.venv\Scripts\Activate.ps1python -m pip install -r requirements.txtpython -m pip freeze > requirements-lock.txtTerminal window deactivaterm -rf .venvpython3 -m venv .venvsource .venv/bin/activatepython -m pip install -r requirements.txtpython -m pip freeze > requirements-lock.txtThe lock file has 13 lines:
httpx,pytest,ruffand the packages that they need. -
Write
README.mdwith every section from the table.
Prove it: a fresh copy
Section titled “Prove it: a fresh copy”You cannot test a README on the computer where you wrote it: your environment, your variables and your memory fill every gap. Test it the way a new person meets it: a fresh copy, a new terminal, and only the README.
-
Open a new terminal. Do not activate anything.
-
Make a copy of the project without the files that
.gitignorelists. If you use Git and you committed your work, clone it:Terminal window cd ~/projectsgit clone ticket-cleaner ticket-cleaner-freshWithout Git, copy the
ticket-cleanerfolder in your file manager, name the copyticket-cleaner-fresh, and delete.venv,reportsand every__pycache__folder in the copy. -
cd ticket-cleaner-fresh, openREADME.md, and do exactly what it says, nothing more. Do not use a command from your memory. -
Each time that you must do something that the README does not say, stop. Add the missing step to the README in the original project.
-
When the README is complete, delete
ticket-cleaner-freshand do it again. Continue until a fresh copy works with the README only. The program prints15 rows: 9 valid, 6 rejected., andpython -m pytestandruff check .pass.
Your turn: a handoff review
Section titled “Your turn: a handoff review”Ask a person who has not seen the project to do the fresh-copy test, on their own computer, if possible with a different operating system. Do not help them. Write down each place where they stop or ask a question.
If nobody is available, do it on a different computer. Or wait one day, and do it again yourself. A gap that you missed today is easier to see tomorrow.
Show the questions that people ask most often
- “Which Python?” → Requirements needs the version and the command to check it.
- “The prompt does not show (.venv).” → Set up needs the activate command for each system, and the PowerShell execution-policy fix from Isolate dependencies.
- “Where is the report?” → Run needs the path of the output, and the expected output.
- “Is a WARNING line a problem?” → Run needs one sentence: each rejected record appears as a
WARNING, and that is expected. - “How do I run the tests?” → Work on the code.
Common mistake
A lock file with packages that the project does not use
Symptom: requirements-lock.txt has many more lines than expected, for example ipykernel, jupyter_client, traitlets and twenty more. Or a fresh install takes a long time, or fails on a different system.
Cause: pip freeze lists every package in the environment, not only the ones in requirements.txt. Perhaps you installed something to try it, for example ipykernel for a notebook in Notebook discipline. Now it is in the lock file, and every new person installs it too.
Fix:
- Make the lock file in a new environment, with only
requirements.txtinstalled (step 5 of the guided practice). - Compare the old and the new lock file. Every line must come from a package in
requirements.txt, or from what one of them needs. - Install personal tools, such as
ipykernel, outside the project’s environment, or install them again after you make the lock file.
Check your understanding
ruff check --fix removed an import and sorted the others. How do you know that it did not break anything?
Show the answer
Run the tests: python -m pytest. Formatting and safe fixes do not change what the code does, but the tests prove it, instead of trusting it. That is why the README says to run all three: tests, format and check.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Module check: what does it mean that the project runs in a fresh environment using only the README, and how do you show it?
Show the answer
A copy without .venv, reports or any setting from your terminal, set up by following the README step by step, with no step from memory. You show it with the output of that run: the program’s 15 rows: 9 valid, 6 rejected., python -m pytest passing, and ruff check . passing, all in the fresh copy. Each gap that you found is now a step in the README.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- A formatter makes the layout standard. A linter finds patterns that are often bugs. ruff does both:
ruff format .andruff check .. - After any automatic change, run the tests.
- A clean layout has only the files that the project needs.
.gitignorelists what stays private or is made again. - The README says what it does, what you need, how to set up and run it, and what you see when it works.
- Make the lock file in a new environment. Test the README on a fresh copy, and add every missing step.
You can now build, test and hand over a Python project. The final assignment puts it all together.
Next: Final assignment