Skip to content

Independent comprehension

  • Lesson 15
  • 45 min
  • Module 5: Deliver maintainable work

What you will be able to do

Before you ask anyone to review a change, explain every line that changes behaviour, without help. Show which test guards each line, open a pull request that a reviewer can check alone, and name what only a person can still decide.

Before you startWhat to know and to have readyAbout 45 minutes

What you need to know

What you need on your computer

  • The project ~/projects/ticket-cleaner-assist after the last lesson, with its environment active. python -m pytest prints 42 passed, and git status --short prints nothing.
  • main has only the course’s start. Check it: git diff --stat main...feature/category-option -- ticket_cleaner/ ends with 3 files changed, 34 insertions(+), 7 deletions(-). If it does not, follow the reset steps under Module check.
  • About 40 MB of free space, for a second copy of the project with its own environment.
  • No account, no website and no money. The remote is a folder on your computer.

A guest at a restaurant asks: “Is there any nut in this sauce?” A helper made the sauce, not the cook. “I did not make it” is not an answer that the guest can use. The cook who serves the dish answers for every ingredient. So a careful cook knows what went in before the plate leaves the kitchen.

A pull request is the plate leaving the kitchen. The assistant was the helper.

The problem: “the tests pass, merge it”

Section titled “The problem: “the tests pass, merge it””

Your branch has the whole change of this course: the option, the fix, the refactor and the documents. All 42 tests pass. Tomás reads the result:

“Forty-two green tests. What else is there to explain? Open the pull request.”

Here is one thing that no test and no task mentioned. Run the sample file’s pending report on main, then on your branch:

Terminal window
git switch main
python -m ticket_cleaner data/tickets.csv --status pending
git switch feature/category-option
python -m ticket_cleaner data/tickets.csv --status pending

Open reports/summary.json after each run. The author’s results:

by_category of the pending report
main: {"LOGIN": 1, "billing": 1}
feature/category-option: {"billing": 1, "login": 1}

The sample file has one ticket stored as LOGIN (T-1010, line 11 of data/tickets.csv). The fix of Module 4 changed how every category is stored, so this report changed too. That is correct: it is the bug fix doing its job. But Grace’s team may compare this month’s report with last month’s. A LOGIN line that disappears will look like a lost ticket, unless someone tells them.

The baseline test did not catch it, because it pins only the default open report. So “the tests pass” did not tell you about it. Only reading the line, and asking what else it touches, did.

The idea: explain every consequential line

Section titled “The idea: explain every consequential line”

Independent comprehension means that you can explain your final change without the assistant, line by line, to a reviewer who asks. Not every line needs the same attention. A consequential line changes behaviour, a contract, stored data or security: a condition, a default, a key in the report, a cleaning rule. A blank line, a docstring or a reformatted call is not consequential, although you still read it.

For each consequential line, you can answer four questions:

  1. What does it do, with a concrete input?
  2. Why is it there: which requirement or bug needs it?
  3. Which test fails without it?
  4. What else does it touch?

The first two go into a traceability table: each requirement, the change that meets it, and the evidence. You met the first half of it in Problem and acceptance criteria. The pull request completes it with the diff and the tests.

A table with three columns: requirement, change and evidence. 1, count only the chosen categories: report.py build_summary with categories; test_one_category. 2, the option can be given more than once: cli.py action append; test_a_ticket_in_any_chosen_category_counts and test_report_for_some_categories. 3, capital letters and spaces do not matter: report.py clean_category of the name; test_category_name_ignores_capital_letters_and_spaces. 4, categories key only with the option: report.py, if chosen is not None; test_categories_key_only_with_the_option and tests/test_baseline.py. 5, whole-file counts unchanged: report.py filters after the totals; test_categories_do_not_change_the_whole_file_counts. 6, unknown category is not an error: no code needed; test_unknown_category_selects_nothing. 7, help and README describe the option: cli.py help and README.md; test_help_describes_the_category_option and tests/test_readme.py. Bug, one line per team: parsing.py clean_category cleans first; test_capital_letters_do_not_make_a_new_category, which failed before the fix, and test_web_form_export_has_one_line_per_team. Footer: a row without a test is a claim; a test without a row is a change that nobody asked for.
Requirement → change → evidence, as in docs/pull-request.md. The whole branch is 16 files, +536 −9, but the code part is three files, +34 −7.
Text description of the diagram
Requirement Change (file) Evidence (test)
1. Count only the chosen categories report.py: build_summary(..., categories) test_one_category
2. The option can be given more than once cli.py: action="append" test_a_ticket_in_any_chosen_category_counts, test_report_for_some_categories
3. Capital letters and spaces do not matter report.py: clean_category(name) test_category_name_ignores_capital_letters_and_spaces
4. categories key only with the option report.py: if chosen is not None test_categories_key_only_with_the_option, tests/test_baseline.py
5. Whole-file counts unchanged report.py: filters after the totals test_categories_do_not_change_the_whole_file_counts
6. Unknown category is not an error (no code needed) test_unknown_category_selects_nothing
7. Help and README describe the option cli.py help, README.md test_help_describes_the_category_option, tests/test_readme.py
Bug: one line per team parsing.py: clean_category cleans first test_capital_letters_do_not_make_a_new_category (it failed before the fix), test_web_form_export_has_one_line_per_team

How do you know that a test really guards a line? Change the line on purpose, run the tests, and put it back. If no test fails, the line has no guard, or the line does nothing. Testers call a systematic form of this mutation testing. By hand, for a few lines, it takes minutes.

Take requirement 2. In ticket_cleaner/cli.py, delete one line from the --category option, and save:

ticket_cleaner/cli.py (changed on purpose)
parser.add_argument(
"--category",
action="append",
metavar="NAME",

Predict which tests fail. Then run them:

Terminal window
python -m pytest
Output (the end)
FAILED tests/test_cli.py::test_report_for_some_categories - ...
========================= 1 failed, 41 passed in 0.05s =========================

One test. Without action="append", args.category is one text instead of a list, and the last --category wins. The unit tests in test_report.py call build_summary() with a list, so they cannot see it. Only the test that runs the whole command can. Put the line back, and check that nothing else changed:

Terminal window
git restore ticket_cleaner/cli.py
git status --short

git status --short prints nothing. The author did the same for six consequential lines:

A table of six lines changed on purpose, the tests that fail, and the result. parsing.py, the fix, text equals clean_text of value changed back to str of value stripped: test_capital_letters_do_not_make_a_new_category, Grace's export, 3 cleaning cases, Sign-in and 4 tests of the option; 10 failed, 32 passed. report.py, clean_category of name changed to name: capital letters and spaces, the categories key, Sign-in and test_report_for_some_categories; 4 failed, 38 passed. report.py, clean_category of name changed to name strip lower: only test_web_form_names_work_in_the_category_option, the test of this module; 1 failed, 41 passed, in amber. report.py, the categories key in every report: tests/test_baseline.py and test_categories_key_only_with_the_option; 2 failed, 40 passed. report.py, valid_records equals the length of the selection: tests/test_baseline.py, test_report_for_some_categories and 2 whole-file tests; 4 failed, 38 passed. cli.py without action append: test_report_for_some_categories; 1 failed, 41 passed, in amber. Footer: every consequential line has a test that fails without it; for two lines it is one test, do not delete it.
Six consequential lines, each changed on purpose at the end of the module. Every one has a guard; two have exactly one.
Text description of the diagram
Line changed on purpose Result
parsing.py: text = clean_text(value) → str(value).strip() (undo the fix) 10 failed, 32 passed
report.py: clean_category(name) → name 4 failed, 38 passed
report.py: clean_category(name) → name.strip().lower() (the rule of Module 4) 1 failed, 41 passed: only test_web_form_names_work_in_the_category_option
report.py: the categories key in every report 2 failed, 40 passed: the baseline test and test_categories_key_only_with_the_option
report.py: "valid_records": len(selected) 4 failed, 38 passed
cli.py: no action="append" 1 failed, 41 passed: only test_report_for_some_categories

This is the module’s practice task: submit the feature and the defect fix as a reviewable practice pull request. As in the Git course, it is a local simulation. The remote is a bare repository in a folder, and you play both people: Amira, who opens it, and Kwame, who reviews it.

  1. See the whole change at once. Two dots list the commits on the branch; three dots show the changes since the branch started:

    Terminal window
    git log --oneline main..feature/category-option
    git diff --stat main...feature/category-option -- ticket_cleaner/
    Output (the author's run)
    51b4ddf Document the cleaning rule, the limitations and the decisions
    23d515b Clean the --category names with the same rule as the tickets
    b52a2f9 Copy the snapshot end-of-m04
    ticket_cleaner/cli.py | 8 +++++++-
    ticket_cleaner/parsing.py | 9 ++++++---
    ticket_cleaner/report.py | 24 +++++++++++++++++++++---
    3 files changed, 34 insertions(+), 7 deletions(-)

    The author’s run made Modules 1 to 4 as one commit, as the reset steps do. Your list has eleven commits: nine from Modules 1 to 4, and two from this module. The --stat line is the same.

  2. Read the code part, with the table above next to it:

    Terminal window
    git diff main...feature/category-option -- ticket_cleaner/

    For each changed line, say aloud: what it does, why, which test, and what else. Lines 31 to 36 of cli.py add the option. Line 71 passes it on. Lines 23 and 24 of parsing.py are the fix. In report.py, line 1 imports the one cleaning rule, lines 38 to 41 choose and filter, and lines 50 and 51 add the categories key. The rest are docstrings and the new signature.

  3. Break one line on purpose, as in the tiny example. Choose a line from the table, but not the one of the example. Predict, run python -m pytest, then git restore the file. Write down the line and the test that caught it. You will use it in the completion check.

  4. Write the pull request description. It lives in the project, as docs/pull-request.md, so that it travels with the branch. Copy it from the snapshot, then read every row against your own runs:

    Terminal window
    cp ../nextia-learning-labs/C15/snapshots/end-of-m05/docs/pull-request.md docs/

    It has the parts of the Git course, and three more: the table, the use of an assistant, and what a reviewer should look at. The full text is below these steps.

  5. Run the checks that the description promises, and commit it:

    Terminal window
    python -m pytest
    ruff check .
    ruff format --check .
    python -m ticket_cleaner data/web-form-export.csv --category Sign-in
    git add docs/pull-request.md
    git commit -m "Describe the pull request"
    Output (part)
    ============================== 42 passed in 0.04s ==============================
    All checks passed!
    24 files already formatted
    [feature/category-option bd7c066] Describe the pull request
    1 file changed, 45 insertions(+)
    create mode 100644 docs/pull-request.md

    The report of the Sign-in command has "by_category": {"login": 4}.

  6. Make the remote next to the project. Use a new name: team-remote.git from the Git course holds ticket-notes, a different project.

    Terminal window
    cd ..
    git init --bare ticket-cleaner-remote.git
    cd ticket-cleaner-assist
    git remote add origin ../ticket-cleaner-remote.git
    Output
    Initialized empty Git repository in ~/projects/ticket-cleaner-remote.git/

    Your output shows the full path of your folder.

  7. Push both branches. The target main must exist on the remote, so that a reviewer can compare:

    Terminal window
    git push -u origin main
    git push -u origin feature/category-option
    Output
    To ../ticket-cleaner-remote.git
    * [new branch] main -> main
    branch 'main' set up to track 'origin/main'.
    To ../ticket-cleaner-remote.git
    * [new branch] feature/category-option -> feature/category-option
    branch 'feature/category-option' set up to track 'origin/feature/category-option'.

    The pull request is feature/category-option → main, and its description is docs/pull-request.md.

Here is the description. Read it as a reviewer would, with the diff open:

docs/pull-request.md
# Pull request: report by category, and one line per team
Branch: `feature/category-option` → `main`
## What and why
- Grace asked for the report of one or a few categories
(`docs/tasks/category-option.md`).
- Grace's bug: with the web form's export, one team appeared under several
names (`docs/tasks/category-names-bug.md`).
## Requirement → change → evidence
| Requirement | Change (file) | Evidence |
|---|---|---|
| 1. Count only the chosen categories | `report.py` `build_summary(..., categories)` | `test_one_category` |
| 2. The option can be given more than once | `cli.py` `action="append"` | `test_a_ticket_in_any_chosen_category_counts`, `test_report_for_some_categories` |
| 3. Capital letters and spaces do not matter | `report.py` uses `clean_category` | `test_category_name_ignores_capital_letters_and_spaces` |
| 4. `categories` key only with the option | `report.py` | `test_categories_key_only_with_the_option`, `test_baseline.py` |
| 5. Whole-file counts unchanged | `report.py` filters after the totals | `test_categories_do_not_change_the_whole_file_counts` |
| 6. Unknown category is not an error | (no code needed) | `test_unknown_category_selects_nothing` |
| 7. Help and README describe the option | `cli.py` help, `README.md` | `test_help_describes_the_category_option`, `test_readme.py` |
| Bug: one line per team | `parsing.py` `clean_category` cleans first | `test_capital_letters_do_not_make_a_new_category` (fails before the fix), `test_web_form_export_has_one_line_per_team` |
## How I checked it
```sh
python -m pytest # 42 passed
ruff check .
ruff format --check .
python -m ticket_cleaner data/web-form-export.csv --category Sign-in
```
## Use of a coding assistant
An assistant suggested the first version of the option and of the fix. I read
every line, ran the tests myself, and changed or rejected some suggestions:
see `DECISIONS.md`. The regression test was written first and failed on the old
code.
## What a reviewer should look at
- `parsing.py`: the fix changes how every category is stored. Reports of older
files can change where a category had capital letters (`LOGIN` → `login`).
- `CATEGORY_ALIASES` is owned by the web form's team.

Two parts are new compared with the Git course. Use of a coding assistant says plainly what the assistant did and what you did. It does not hide the assistant, and it does not blame it. What a reviewer should look at points to the pending report change of the problem above. You found it; now the reviewer does not need to.

On GitHub, the steps are the same up to the push. Then Compare & pull request opens a form: paste the description, set base to main, and select Create pull request. That is optional, and not needed for this course.

Kwame reviews: a fresh clone, a fresh environment

Section titled “Kwame reviews: a fresh clone, a fresh environment”

Kwame does not trust “42 passed” in a description, even from Amira. He runs the checks himself, in a new clone, with a new environment. That is the completion check of this module, and you do it now as Kwame.

  1. Clone the remote into a new folder, next to the project, and switch to the branch:

    Terminal window
    cd ~/projects
    git clone ticket-cleaner-remote.git kwame-review
    cd kwame-review
    git switch feature/category-option
    Output
    Cloning into 'kwame-review'...
    done.
    Switched to a new branch 'feature/category-option'
    branch 'feature/category-option' set up to track 'origin/feature/category-option'.
  2. Make a fresh environment from the lock file. Deactivate the old one first, with deactivate:

    Terminal window
    python3 -m venv .venv
    source .venv/bin/activate
    python -m pip install -r requirements-lock.txt
  3. Run every check of the description, from this clone:

    Terminal window
    python -m pytest
    ruff check .
    ruff format --check .
    python -m ticket_cleaner data/web-form-export.csv --category Sign-in
    python -m ticket_cleaner data/web-form-export.csv
    Output (the author's run, the end of each check)
    ============================== 42 passed in 0.06s ==============================
    All checks passed!
    24 files already formatted

    The first report has "by_category": {"login": 4} and "categories": ["login"]. The second, without the option, has {"account": 1, "billing": 3, "login": 4, "shipping": 2}: one line per team.

  4. Read the change as a reviewer: git diff main...feature/category-option. Kwame asks his usual question: “What can this change touch?” Answer it from the diff. It touches two report keys and one cleaning rule that every ticket goes through. It adds no file access and no package, and it sends nothing outside.

The same results in a fresh clone prove that every needed file is committed, and that nothing depends on your own environment.

Some questions no test can answer, because the answer is outside the code. Name them in the pull request and in DECISIONS.md, with the person who can answer:

Three boxes in a row. ticket-cleaner-assist, Amira's project, on feature/category-option with docs/pull-request.md, 42 passed, ruff clean. An arrow labelled git push -u to ticket-cleaner-remote.git, a bare repository, the team's remote, with main and feature/category-option. An arrow labelled git clone to kwame-review, Kwame's clone, with a new .venv from requirements-lock.txt, 42 passed, and Sign-in gives login 4. Below, three amber boxes: what still needs a human or domain review. Older reports change: the fix changes how every category is stored; the sample file's pending report goes from LOGIN 1 and billing 1 to billing 1 and login 1; who reads the old numbers? The alias list: CATEGORY_ALIASES must match the web form; Grace's team owns the form, so only they can say that it is complete. Real exports: the tests use made-up data; a real export can have other spellings, for example Log in with a space.
The checks travel with the branch, and a fresh clone repeats them. The amber questions do not travel: a person must answer them.
Text description of the diagram

Amira pushes the branch to ticket-cleaner-remote.git. Kwame clones it, makes a new environment from the lock file, and gets 42 passed and {"login": 4} for Sign-in.

Three questions still need a person:

  • Older reports change. The sample file’s pending report goes from {"LOGIN": 1, "billing": 1} to {"billing": 1, "login": 1}. Grace decides how to explain it to her team.
  • The alias list. CATEGORY_ALIASES must match the web form. Grace’s team owns the form.
  • Real exports. The tests use made-up data. A real export can have other spellings, such as Log in with a space.

A domain review is honest about where the code’s knowledge ends. Kwame approves the code, and Grace answers the three questions. Then the branch can be merged, as in the Git course.

Explain every consequential line before every pull request, whoever wrote it. With an assistant, nobody else has read the code. If you cannot explain a line, learn it or remove it.

Break lines on purpose for the lines that matter most: a fix, a condition, a key of the output. It shows which lines have only one guard.

Ask for a domain review when the answer depends on people or data outside the project. Write the question and the person’s name. Do not answer it with a guess.

The cost is time before the review, and it saves more time during it. A traceability table lets a reviewer check a change in minutes. “Fixed stuff, tests pass” sends the reviewer back to you with questions.

Common mistake

git push: does not appear to be a git repository

Symptom: you push the branch for the first time, and Git refuses:

Output
fatal: '../ticket-cleaner-remote.git' does not appear to be a git repository
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.

Cause: the bare repository is in the wrong place. Here, git init --bare ran inside the project, because cd .. was missed. The remote’s path, ../ticket-cleaner-remote.git, points to a folder that does not exist.

Diagnose: git status --short shows the bare repository as a new folder inside the project:

Output
?? ticket-cleaner-remote.git/

Fix: delete that folder inside the project, after you check its name: rm -rf ticket-cleaner-remote.git (Windows: Remove-Item -Recurse -Force ticket-cleaner-remote.git). Then do step 6 again, with cd .. first. Never commit a bare repository into a project.

The module’s completion check: run the checks independently, and explain every consequential change. Show these:

  1. In kwame-review, a fresh clone with a fresh environment, the checks pass. You see 42 passed, All checks passed! and 24 files already formatted, and the two reports of the Kwame steps.
  2. In your own words, for five consequential lines of git diff main...feature/category-option -- ticket_cleaner/, the four answers: what, why, which test, what else. Use at least one line from each of the three files.
  3. The line that you broke on purpose in step 3, and the test that caught it.
  4. One question for a domain review that is not in the pull request yet, with the person who can answer it.
Show a worked solution

Five lines, four answers each:

Line What Why Which test What else
parsing.py 23: text = clean_text(value) " LOGIN " → "login" before the alias lookup Grace’s bug: one line per team test_capital_letters_do_not_make_a_new_category (10 tests fail without it) every category of every file, so older reports change (LOGIN → login)
cli.py 33: action="append" --category billing --category shipping gives a list of both criterion 2 test_report_for_some_categories (the only one) without it, the last option wins, silently
report.py 40: chosen = sorted({clean_category(name) ...}) "Sign-in" → ["login"]; duplicates removed, sorted criteria 3 and 4, and the web form’s names test_web_form_names_work_in_the_category_option and 3 others the categories key shows the cleaned names, not what the user typed
report.py 41: selected = [... if ticket.category in chosen] keeps only the tickets of the chosen categories criterion 1 test_one_category it runs after filter_by_status(), so --status still applies
report.py 50–51: if chosen is not None: adds categories only with the option criterion 4, and Grace’s spreadsheet test_categories_key_only_with_the_option, tests/test_baseline.py the report without the option stays the same as before

A domain question: “Does anything else read reports/summary.json, for example a script of the finance team? If so, its owner must know about the categories key.” The person: Grace, who knows who receives the report.

You have finished the module when you can show these on your own project:

  • git log --oneline shows thirteen commits. The last three are Describe the pull request, Document the cleaning rule, the limitations and the decisions and Clean the --category names with the same rule as the tickets. After a reset, you have fewer commits, and that is fine.

  • python -m pytest prints 42 passed, in your project and in kwame-review.

  • git branch -vv shows both branches with an upstream on origin.

  • On macOS and Linux, this command prints nothing, because your project matches the snapshot end-of-m05:

    Terminal window
    diff -rq . ../nextia-learning-labs/C15/snapshots/end-of-m05 -x .git -x .venv -x reports -x __pycache__ -x .pytest_cache -x .ruff_cache

Reset. If your project is broken, follow the reset steps of Context and boundaries with the snapshot end-of-m04 in place of end-of-m01. They keep main at the course’s start, so the pull request shows the whole change. The last line of python -m pytest is then 39 passed: the project at the end of Module 4, ready for Refactor selectively. To skip to the end of this module, use end-of-m05 instead: 42 passed. If ticket-cleaner-remote.git or kwame-review already exist, rename them too before step 6.

Check your understanding

What makes a line of a change 'consequential'? Give one consequential and one non-consequential line from the code part of this course's change.

Show the answer

A consequential line changes behaviour, a contract, stored data or security. text = clean_text(value) in parsing.py is consequential: it changes how every category is stored. The docstring of build_summary() is not: it changes no behaviour, although a wrong docstring would mislead the next reader, so you still read it. A useful test: if you break the line on purpose, should a test fail? For a consequential line, yes.

Check your understanding

Predict: you change line 40 of report.py back to name.strip().lower(). How many tests fail, and what would you conclude?

Show the answer

One: test_web_form_names_work_in_the_category_option, and 1 failed, 41 passed. The other tests use names that both rules clean the same way. So this line has exactly one guard. Keep that test, and name it in the pull request and in DECISIONS.md, as decision 3 does. If a future change deletes it as “duplicate”, the two-rules bug can come back with all other tests green.

Check your understanding

All 42 tests passed in Amira's folder. Why does Kwame run them again in a fresh clone with a fresh environment, and what could differ?

Show the answer

A passing run in the author’s folder can depend on things that are not in the commits. Examples are a file that was never added, or an edit that was never committed. Another is a package that is in the author’s environment but not in the lock file. A fresh clone has only what was pushed, and a fresh environment has only what the lock file lists. If the same 42 tests pass there, the change is complete and reproducible. It is the same proof as the fresh copy in the Python course.

Recap

This lesson

  • Independent comprehension: you explain every consequential line without the assistant. What it does, why, which test fails without it, and what else it touches.
  • A traceability table links each requirement to its change and its evidence. It is the heart of docs/pull-request.md.
  • Break a line on purpose to see its guard. Two lines of this change have exactly one test.
  • The fix also changes the sample file’s pending report (LOGIN → login). No test caught it; reading did. It is now in the pull request.
  • Kwame’s fresh clone and fresh environment: 42 passed. The alias list, real exports and older reports still need a person.

Module 5: deliver maintainable work

  • Refactor only where the change needs it: one cleaning rule made --category Sign-in work. Everything else became a separate task.
  • Document decisions with their evidence, and write no claim that you did not check. tests/test_readme.py keeps two README claims true.
  • Open a pull request that a reviewer can check alone: the table, the checks, the use of an assistant, and the open questions.
  • Your project matches end-of-m05, the course-end project: 42 passed.

The course

You started with a project, a feature request and a bug report. You prepared a safe setup and wrote a task that someone can verify. You reviewed generated changes line by line, debugged from evidence, and delivered a change that you can explain. Next, the case studies apply the same habits to two small projects that are new to you, written for the course. One has a bug in unfamiliar code; the other needs a dependency upgrade. One of them is required for the certificate, with the final assignment.

Next: Fix a bug in an unfamiliar codebase