Upgrade a dependency safely
- Case study 2
- 150 min
- Optional · 1 case study needed for the certificate
Case study 2 of 2 · AI-Assisted Software DevelopmentUpgrade a dependency safely
1. Establish a safe working setup
2. Write actionable tasks
3. Review generated work
4. Debug and test with assistance
5. Deliver maintainable work
What you will be able to do
Move a small project from one major version of a package to the next. Read the changelog before you install, and save today’s behaviour. Let an assistant propose the code changes. Then check each change against the real changelog and the saved behaviour, pin the new version, and write the decision down.
The course team wrote form-intake for this case study, under the MIT licence. Its 32 web-form submissions are invented. The package that it depends on, marshmallow, is real and widely used, and so are its releases and its changelog. The assistant session is a real recording that comes with the project, so you need no assistant and no account.
Before you startWhat to know and to have ready
What you need to know
This case study uses the whole course on a project that is new to you. It leans most on Verify dependencies, Inspect the diff, Reproduce first, Choose meaningful checks and Document decisions. The lock file comes from Isolate dependencies in the Python course, and branches come from the Git course.
What you need on your computer
- Python 3.12 or newer, Git, and VS Code.
- About 60 MB of free disk space, and an internet connection for
pip. - No account and no money. An assistant is optional: the recorded session is in the download.
Tested on macOS (Apple silicon) with Python 3.14.6 and pip 26.1.2. The Windows commands are written, not tested. The commit IDs on your computer will differ from the ones here.
The situation
Section titled “The situation”Grace, the support lead, sends Amira two rejected submissions from the web form:
“Two customers pressed Help, and
form-intakerejected them with ‘Not a valid URL.’ Both pages exist:https://larkfield.example#helpon the home page, andhttps://café.larkfield.example/helpon the café shop. Can you fix it?”
form-intake checks each submission of Larkfield’s web form before it reaches the help desk. It describes the rules with marshmallow, a package that checks data against a schema: a list of fields and their rules. marshmallow’s own URL rule checks the web addresses, not Larkfield’s code.
Amira reads marshmallow’s changelog. Newer releases accept both addresses: international domain names since 4.2.4, and #help straight after the domain since 4.3.1. form-intake pins 3.26.2. So the fix is an upgrade across a new major version, from 3 to 4.
Tomás has a quick plan: “Change the 3 to a 4, let the assistant fix whatever breaks, and merge it when the tests pass.” Kwame asks: “What else does version 4 change, and how will we know?”
The objective. Move form-intake to marshmallow 4.3.1 so that:
- Grace’s two submissions are accepted.
- Every other decision stays the same, unless the team chooses to change it and writes down why.
- Each code change answers a real entry of marshmallow’s changelog.
- The tests pass, with no warnings left.
- The new version is pinned, and the decision is recorded.
What you will do
Section titled “What you will do”Text description of the diagram
Nine steps, from Grace’s problem to new input. The tests pass at step 5, after the assistant’s change. The review at step 6 finds six time formats that now get another decision. No test had noticed them.
The project and its licence
Section titled “The project and its licence”| File | What it does |
|---|---|
form_intake/schema.py |
The rules, as a marshmallow schema, and check_submissions() |
form_intake/cli.py |
The command: reads a JSON file, writes out/accepted.json and out/rejected.json |
tests/ |
13 tests of the rules and the command |
data/submissions.json |
12 invented submissions, among them Grace’s two |
data/time-formats.json |
10 ways to write the time of a submission, one submission each |
data/new-submissions.json |
10 more submissions, for the end |
tools/compare.py |
Lists the submissions whose decision changed between two runs |
DECISIONS.md, README.md |
The choices, and the rules in a table |
The project is MIT, “Copyright (c) Nextia Learning”. marshmallow is MIT too, by Steven Loria and contributors. pip downloads it from PyPI; it is not in the zip. The download’s dataset.md card lists what is real and what is constructed.
Set up the project
Section titled “Set up the project”-
Download the starter project from the box at the top of this page. Make a folder
~/projects/upgrade-case-study, and unzip the file into it. You get two folders:form-intakeandassistant-session. -
In VS Code, choose File > Open Folder, and open
form-intake. Then open a terminal: Terminal > New Terminal. -
Make and activate a virtual environment. Then install the exact versions from the lock file:
Terminal window python3 -m venv .venvsource .venv/bin/activatepython -m pip install -r requirements-lock.txtTerminal window python -m venv .venv.venv\Scripts\Activate.ps1python -m pip install -r requirements-lock.txtTerminal window python3 -m venv .venvsource .venv/bin/activatepython -m pip install -r requirements-lock.txt -
Run the tests and the program:
Terminal window python -m pytestpython -m form_intake data/submissions.jsonpython -m form_intake --versionOutput (the last lines) ======================= 13 passed, 20 warnings in 0.03s ========================12 submissions: 3 accepted, 9 rejected.Saved: out/accepted.json, out/rejected.jsonform_intake 1.0.0 (marshmallow 3.26.2) -
Make the project a Git repository with one commit, as in the Git course:
Terminal window git initgit add -Agit commit -m "Start"
What this tells us. The tests pass, but with 20 warnings. You read them in Part 2. Open out/rejected.json: T-4007 and T-4011 are Grace’s two submissions, rejected with "page_url": ["Not a valid URL."].
Version numbers and breaking releases
Section titled “Version numbers and breaking releases”A new edition of the recipe book
Section titled “A new edition of the recipe book”Your favourite recipe book has a new edition. Most pages are the same. But the new edition no longer has the sauce of your Sunday dinner, and it bakes the cake at a lower heat. If you cook from it without reading its foreword, Sunday’s dinner fails and the cake is pale. Nobody warns you at the stove. But the publisher did warn you: in the foreword, and in a note in the last printing of the old edition.
The problem: one number, and nothing runs
Section titled “The problem: one number, and nothing runs”Tomás’s plan is to change marshmallow==3.26.2 to 4.3.1 and see. The author did exactly that, on a branch (Part 4 shows it). Not one test ran:
E TypeError: Field.__init__() got an unexpected keyword argument 'missing'!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!!That error is the loud kind. The quiet kind is worse: a release can change what the code does, with no error at all. You find those only if you know where to look.
The idea: the version number tells you where to look
Section titled “The idea: the version number tells you where to look”Most Python packages number their releases MAJOR.MINOR.PATCH. The scheme is called semantic versioning. A new PATCH number (4.3.0 → 4.3.1) fixes bugs. A new MINOR number (4.2 → 4.3) adds features. It may also deprecate something: it still works, but it prints a warning that it will be removed. A new MAJOR number (3 → 4) may remove or change things: a breaking release. The project’s changelog lists the changes of every release. marshmallow marks the breaking ones Backwards-incompatible, and it has an upgrading guide.
So a new MAJOR number means: read the changelog before you install. And today’s deprecation warning is the next MAJOR version’s error.
A tiny example: the releases that matter to form-intake
Section titled “A tiny example: the releases that matter to form-intake”Text description of the diagram
A list of releases from marshmallow’s changelog. Four 3.x MINOR releases deprecated things that form-intake uses. 4.0.0 removed them all, and changed how times are read. 3.26.2, the version that form-intake pins, is a PATCH with a security fix, and it came out after 4.0.0. 4.2.4 and 4.3.1 fixed the two web addresses.
Two details in the picture matter. First, Grace’s fixes are in 4.2.4 and 4.3.1, not in 4.0.0. The author checked: 4.0.0 still rejects both addresses. Second, 3.26.2 is newer than 4.0.0 by date. A project can keep fixing an old MAJOR line for a while, for teams that have not moved yet. Use the version number, not the date, to judge what changed.
The four checks of Verify dependencies also apply to a new version of a package that you already use. It exists. You need it, for Grace’s fixes. Its licence is still MIT. And it supports your Python: the 4.1.0 changelog says “Test against Python 3.14”.
Part 1: Grace’s problem as tests
Section titled “Part 1: Grace’s problem as tests”As in Reproduce first, the evidence comes before the change. Add import pytest to the imports of tests/test_schema.py, and this test at the end:
@pytest.mark.parametrize( "page_url", ["https://larkfield.example#help", "https://café.larkfield.example/help"],)def test_page_addresses_from_the_web_form_are_accepted(page_url): accepted, rejected = check(page_url=page_url) assert rejected == [] assert accepted[0].page_url == page_urlGrace also expects the counts of the whole file to change. In tests/test_cli.py, change three expected values:
- the output:
12 submissions: 5 accepted, 7 rejected. - the accepted list:
["T-4001", "T-4002", "T-4007", "T-4011", "T-4012"] - the number of rejected submissions:
7
Then run the tests:
python -m pytest -qFAILED tests/test_cli.py::test_cli_writes_accepted_and_rejected - AssertionEr...FAILED tests/test_schema.py::test_page_addresses_from_the_web_form_are_accepted[https://larkfield.example#help]FAILED tests/test_schema.py::test_page_addresses_from_the_web_form_are_accepted[https://caf\xe9.larkfield.example/help]3 failed, 12 passed, 22 warnings in 0.04sThree tests fail for the right reason: 'page_url': ['Not a valid URL.']. Commit them with git commit -am "Describe Grace's page addresses as tests". These tests now say what “done” means. Nobody may change them to make an upgrade pass.
Part 2: read before you install
Section titled “Part 2: read before you install”The warnings
Section titled “The warnings”The 20 warnings of the first run are the old edition’s note. List each different one:
python -m pytest -q 2>&1 | grep -E 'Warning:' | sed -e 's/^ *//' | sort | uniq -cpython -m pytest -q 2>&1 | Select-String 'Warning:' | Sort-Object -Uniquepython -m pytest -q 2>&1 | grep -E 'Warning:' | sed -e 's/^ *//' | sort | uniq -c 1 fields.py:1753: RemovedInMarshmallow4Warning: The 'missing' argument to fields is deprecated. Use 'load_default' instead. 1 fields.py:276: ChangedInMarshmallow4Warning: Returning `False` from a validator is deprecated. Raise a `ValidationError` instead. 1 fields.py:986: RemovedInMarshmallow4Warning: The 'missing' argument to fields is deprecated. Use 'load_default' instead. 1 schema.py:129: RemovedInMarshmallow4Warning: The `ordered` `class Meta` option is deprecated. Field order is already preserved by default. Set `Schema.dict_class` to OrderedDict to maintain the previous behavior. 1 ~/projects/upgrade-case-study/form-intake/form_intake/cli.py:28: DeprecationWarning: The '__version__' attribute is deprecated and will be removed in in a future version. Use feature detection or 'importlib.metadata.version("marshmallow")' instead. 1 ~/projects/upgrade-case-study/form-intake/form_intake/schema.py:46: RemovedInMarshmallow4Warning: The 'missing' argument to fields is deprecated. Use 'load_default' instead. 1 ~/projects/upgrade-case-study/form-intake/form_intake/schema.py:64: RemovedInMarshmallow4Warning: The `context` parameter is deprecated and will be removed in marshmallow 4.0. Use `contextvars.ContextVar` to pass context instead.The warning names say it: RemovedInMarshmallow4, ChangedInMarshmallow4. You can also ask Python to treat these warnings as errors. Then you see the first failure of version 4 before you install it:
python -m pytest -q -W error::DeprecationWarningE marshmallow.warnings.RemovedInMarshmallow4Warning: The 'missing' argument to fields is deprecated. Use 'load_default' instead.!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!!The changelog
Section titled “The changelog”Now open the 4.0.0 section of marshmallow’s changelog and the upgrading guide. The changelog is long, and most of it does not touch form-intake. Search the project for each entry’s name: in VS Code, Edit > Find in Files, for example missing= or context. Keep only the entries that hit a line of your code. Then write down what you expect each one to do:
Text description of the diagram
Eight rows. Four entries stop the program with an error: missing, data_key, context and __version__. Three change what it does without an error: a validator that returns False, the ordered option, and a date without a time. The last row is the change that Grace needs.
Here is the most important quote for this project, from the 4.0.0 changelog:
Backwards-incompatible: Use
datetime.date.fromisoformat,datetime.time.fromisoformat, anddatetime.datetime.fromisoformatfrom the standard library to deserialize dates, times and datetimes. As a consequence of this change: Time with time offsets are now supported. YYYY-MM-DD is now accepted as a datetime and deserialized as naive 00:00 AM.
form-intake has a test for exactly that: test_date_without_time_is_rejected. But a changelog lists the consequences that its authors noticed. It cannot list every input that your program meets. That is the reason for Part 3.
Part 3: save today’s behaviour
Section titled “Part 3: save today’s behaviour”In Repository preparation, the baseline was the known state before a change. The tests pin the rules that someone wrote down. But a program also makes decisions that no test describes, and an upgrade can change them. So keep a second baseline: today’s decisions, on more inputs than the tests have.
data/time-formats.json has one submission for each of 10 ways to write a time. The form sends only the first one. The others are a probe: inputs chosen to find out what the code does. Run the probe and the 12 submissions with 3.26.2, and keep the results:
python -m form_intake data/time-formats.json --output-dir out/before/time-formatspython -m form_intake data/submissions.json --output-dir out/before/submissions10 submissions: 4 accepted, 6 rejected.Saved: out/before/time-formats/accepted.json, out/before/time-formats/rejected.json12 submissions: 3 accepted, 9 rejected.Saved: out/before/submissions/accepted.json, out/before/submissions/rejected.json| Submission | submitted_at |
3.26.2 |
|---|---|---|
| T-5001 | 2026-10-09T08:12:00 (what the form sends) |
accepted |
| T-5002 | 2026-10-09 08:12:00 |
accepted |
| T-5003 | 2026-10-09T08:12 |
accepted |
| T-5004 | 2026-10-09 |
rejected |
| T-5005 | 2026-10-09T08 |
rejected |
| T-5006 | 20261009T081200 |
rejected |
| T-5007 | 2026-W41-5T08:12 (a week date) |
rejected |
| T-5008 | 2026-10-09T08:12:00,5 |
rejected |
| T-5009 | 2026-10-09T0812 |
rejected |
| T-5010 | 2026-1-9T8:05 |
accepted |
out/ is in .gitignore, so these results stay on your computer and out of the commits.
Part 4: the new version, on a branch
Section titled “Part 4: the new version, on a branch”-
Make a branch for the upgrade:
Terminal window git switch -c upgrade/marshmallow-4 -
In both
requirements.txtandrequirements-lock.txt, changemarshmallow==3.26.2tomarshmallow==4.3.1. Nothing else in the lock file changes: on Python 3.11 and newer, 4.3.1 needs no other package. -
Install from the lock file:
Terminal window python -m pip install -r requirements-lock.txtOutput (part) Installing collected packages: marshmallowAttempting uninstall: marshmallowFound existing installation: marshmallow 3.26.2Uninstalling marshmallow-3.26.2:Successfully uninstalled marshmallow-3.26.2Successfully installed marshmallow-4.3.1 -
Run the tests and the program:
Terminal window python -m pytest -qpython -m form_intake --versionOutput (end of each) E TypeError: Field.__init__() got an unexpected keyword argument 'missing'...!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!!!2 errors in 0.07scategory = fields.Str(missing="other", validate=validate.OneOf(CATEGORIES))TypeError: Field.__init__() got an unexpected keyword argument 'missing'
This is the baseline of the upgrade: the new version with no code change. You predicted it in Part 2. Notice what it does not show. Python stops at the first error when a module loads. So this one error hides the other three loud ones, and the silent ones cannot show at all.
Part 5: ask the assistant
Section titled “Part 5: ask the assistant”A migration is a good task for an assistant: many small changes that the changelog describes. Make the task bounded, as in Problem and acceptance criteria. Give what changed, the evidence, the files that it may change, and the rules. Amira sent this prompt to Aider, an open-source assistant in a terminal, with the larger model of the course:
We are upgrading marshmallow from 3.26.2 to 4.3.1 (a new major version). requirements.txt andrequirements-lock.txt already say 4.3.1, and it is installed. Now the tests fail:
```E TypeError: Field.__init__() got an unexpected keyword argument 'missing'...2 errors in 0.06s```
Task: change form_intake/schema.py and form_intake/cli.py so that the project works with marshmallow 4.3.1.
Rules:- Do not change the tests or the requirements files. The tests describe the behaviour that we want. The two tests of the page addresses failed with 3.26.2 on purpose: 4.3.1 should make them pass.- Keep every other behaviour the same.- Do not add a dependency.- For each change, name the change in marshmallow 4 that makes it necessary.The last rule matters most: it asks for a reason that you can check. The assistant could change schema.py and cli.py. It could only read the tests and requirements.txt, and it could run the tests. The whole session is in assistant-session/upgrade-marshmallow-4.md, with the exact prompt and reply.
What happened: one model call, about 24 seconds, 4,664 tokens in and 1,529 out. That is about US$0.025 at the price that the course team paid. Prices change, and you pay nothing to read the transcript. The assistant saw only the first error. Yet it changed all seven places, in six numbered sections, each with a reason. Two of them:
### 1. Validators must raise `ValidationError`
Marshmallow 4 no longer treats a validator returning `False` as a validation failure. Keep the previous default error message.
### 2. Preserve the requirement for a time component
Marshmallow 4 uses `datetime.fromisoformat`, which also accepts date-only strings. Add a datetime field that retains the requirement for a date/time separator.Then Aider ran the tests: 15 passed. Apply the same change to your project, and check it:
git apply ../assistant-session/upgrade-marshmallow-4.patchgit diff --stat -- form_intakepython -m pytest -qpython -m pytest -q -W errorruff check .ruff format --check . form_intake/cli.py | 5 ++--- form_intake/schema.py | 37 +++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 15 deletions(-)............... [100%]15 passed in 0.04s............... [100%]15 passed in 0.04sAll checks passed!9 files already formattedEvery test passes, also with warnings as errors, and ruff is clean. Tomás: “Merge it.” Not yet. The tests were written for 3.26.2, and they check only what someone thought to write down.
Part 6: review each change against the changelog
Section titled “Part 6: review each change against the changelog”Open the diff with git diff, or in the Source Control view of VS Code. Take one change at a time, as in Inspect the diff. Ask two questions of each one. Does its reason match a real entry of the changelog? And does it keep the behaviour that the objective asks for?
Text description of the diagram
Seven rows, one per change. All seven reasons match a real entry of the changelog: in this recording, the assistant invented no API and no entry. Five changes are accepted as they are. One, the context, is accepted with a recorded reason. The last one, the date check, passes its test but fails the comparison with 3.26.2.
Two changes need a decision
Section titled “Two changes need a decision”The context. marshmallow 4 removed Schema(context=...). The changelog and the guide suggest Python’s contextvars, or marshmallow’s new Context class, which they call experimental. The assistant did something else: it gave the schema a constructor argument, SubmissionSchema(now=now). That is plain Python, easy to test, and it avoids an experimental API. Accept it, and write the reason down in Part 8. A reviewer who only compares the diff with the guide would call it wrong. It is a choice.
The date check. The reason is right: marshmallow 4 accepts a date alone. But is “every other behaviour the same”? Compare the decisions with the ones that you saved in Part 3:
python -m form_intake data/time-formats.json --output-dir out/after/time-formatspython tools/compare.py out/before/time-formats out/after/time-formats10 submissions: 8 accepted, 2 rejected.Saved: out/after/time-formats/accepted.json, out/after/time-formats/rejected.jsonSame decision: 4Accepted before, rejected now: T-5010Rejected before, accepted now: T-5005, T-5006, T-5007, T-5008, T-5009Six of the ten decisions changed. The assistant’s check stops a date alone: the one case that the changelog names and the test checks. But datetime.fromisoformat accepts more than marshmallow 3 did. It accepts an hour without minutes (2026-10-09T08), no hyphens (20261009T081200), a week date, a comma, and no colon. And it refuses one-digit months and hours (2026-1-9T8:05), which 3.26.2 accepted.
Did the assistant get it wrong? It did what the evidence in front of it asked: the tests pass. The behaviour that changed was not written down anywhere, so neither the tests nor the assistant could see it. Your saved baseline could.
Part 7: the reviewed change
Section titled “Part 7: the reviewed change”Larkfield’s rule for the time is in the README: a date and a time. So write that rule in the code, instead of leaving it to the library. In form_intake/schema.py, add import re at the top, and change the assistant’s SubmissionDateTime:
# Our rule for submitted_at: a date and a local time, as the form sends them.# marshmallow 4 reads times with datetime.fromisoformat, which also accepts a# date alone and other ISO 8601 forms (changelog, 4.0.0). So the rule is ours.LOCAL_TIME = re.compile(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d{1,6})?)?")
class SubmissionDateTime(fields.DateTime): """Require a time component as well as a date.""" """A date and a local time: YYYY-MM-DD, T or a space, HH:MM, seconds if any."""
def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, str) and "T" not in value and " " not in value: if not isinstance(value, str) or not LOCAL_TIME.fullmatch(value): raise self.make_error("invalid", input=value, obj_type=self.OBJ_TYPE) return super()._deserialize(value, attr, data, **kwargs)The pattern reads: four digits, two, two, then T or a space, then hours and minutes, and seconds if they are there. A custom field that changes _deserialize is the way that marshmallow’s own guide writes one.
Then pin each form with a test, so that the next upgrade cannot change it without a red test. Add to tests/test_schema.py:
@pytest.mark.parametrize( "submitted_at", ["2026-10-09T08:12:00", "2026-10-09 08:12:00", "2026-10-09T08:12"],)def test_date_and_local_time_are_accepted(submitted_at): accepted, _ = check(submitted_at=submitted_at) assert accepted[0].submitted_at == datetime(2026, 10, 9, 8, 12)
@pytest.mark.parametrize( "submitted_at", [ "2026-10-09", # a date alone: marshmallow 4 accepts it "2026-10-09T08", # the hour alone: marshmallow 4 accepts it "20261009T081200", # ISO 8601 without hyphens: marshmallow 4 accepts it "2026-W41-5T08:12", # a week date: marshmallow 4 accepts it "2026-10-09T08:12:00+02:00", # a time zone: 3.26.2 crashed on it "2026-1-9T8:05", # one-digit parts: 3.26.2 accepted them ],)def test_other_time_formats_are_rejected(submitted_at): assert problems(submitted_at=submitted_at) == { "submitted_at": ["Not a valid datetime."] }Run everything again, and compare both inputs with the baseline:
python -m pytest -q -W errorruff check .ruff format --check .python -m form_intake data/time-formats.json --output-dir out/after/time-formatspython tools/compare.py out/before/time-formats out/after/time-formatspython -m form_intake data/submissions.json --output-dir out/after/submissionspython tools/compare.py out/before/submissions out/after/submissions........................ [100%]24 passed in 0.05sAll checks passed!9 files already formatted10 submissions: 3 accepted, 7 rejected.Same decision: 9Accepted before, rejected now: T-5010Rejected before, accepted now: none12 submissions: 5 accepted, 7 rejected.Same decision: 10Accepted before, rejected now: noneRejected before, accepted now: T-4007, T-4011What this tells us. Of the 22 saved decisions, three changed, each on purpose. T-4007 and T-4011 are Grace’s two submissions. T-5010, 2026-1-9T8:05, is now rejected, because Larkfield’s rule asks for two digits; the form never sends one. That is a choice, so it goes into DECISIONS.md.
The fair comparison
Section titled “The fair comparison”Four versions of form-intake ran, each in a fresh folder with a fresh environment from its own lock file. All four got the same inputs: the 12 submissions and the 10 time formats. One measure: how many of the 22 decisions differ from Larkfield’s written rule. The rule is the README’s table, input by input: Grace’s two pages accepted, a date and a time with two-digit parts, and so on.
| Version | marshmallow | Tests | Does it run? | Decisions that differ from the rule |
|---|---|---|---|---|
| A. Stay on 3.26.2 | 3.26.2 | 3 failed, 12 passed, 22 warnings | yes | 3: Grace’s two, and T-5010 |
| B. Change the number only | 4.3.1 | 2 errors | no: TypeError |
– |
| C. The assistant’s change | 4.3.1 | 15 passed | yes | 5: T-5005 to T-5009 |
| D. The reviewed change | 4.3.1 | 24 passed | yes | 0 |
Text description of the chart
Of 22 decisions, staying on 3.26.2 differs from the rule on 3. The assistant’s change differs on 5, and the reviewed change on 0. Changing only the version number gives no decisions at all: the program does not start.
What this tells us.
- Staying is safe for the time formats and wrong for Grace. Her two customers stay rejected, and the warnings remain.
- Changing the number only is the worst option. Nothing runs, and the first error hides the others.
- The assistant’s change fixes everything that the tests describe, with real reasons from the changelog. Yet it makes five decisions that nobody chose. “All tests pass” was true, and not enough.
- The reviewed change meets the objective. Be honest about why it scores zero: its time rule is the written rule, put into code. The measure shows that the code now does what the README says, on these 22 inputs. It cannot show what the form might send one day.
Recommendation: version D. It keeps the assistant’s six correct changes and replaces one. Each change has a changelog entry. Three decisions changed on purpose, and each one is written down.
Part 8: pin and record
Section titled “Part 8: pin and record”Both requirements files already pin marshmallow==4.3.1, and nothing else in them changed. Now write the decision. Copy decision 2 from the finished project’s DECISIONS.md, or write your own with the same parts. Here is the start:
## 2. Upgrade marshmallow from 3.26.2 to 4.3.1
**Why.** Grace's team saw two real submissions rejected with "Not a validURL.": `https://larkfield.example#help` and`https://café.larkfield.example/help`. Both are fixed in marshmallow 4(changelog: 4.2.4, "`URL` and `Email` accept Internationalized Domain Names";4.3.1, "`URL` accepts a fragment that follows an empty path"). The 3.x linehas had no release since 3.26.2.Next comes a table: each change in our code, next to its changelog entry. Then how it was checked (24 tests, no warnings, the two comparisons). Then what was changed or not taken (the date check, the experimental Context). Last comes the time-zone crash that the upgrade found. In README.md, write the time rule into the rules table. Then commit:
git add -Agit commit -m "Upgrade marshmallow to 4.3.1"git diff --stat main DECISIONS.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- README.md | 8 +++++--- form_intake/cli.py | 5 ++--- form_intake/schema.py | 44 ++++++++++++++++++++++++++++++++------------ requirements-lock.txt | 2 +- requirements.txt | 2 +- tests/test_schema.py | 26 ++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 21 deletions(-)One commit holds the whole upgrade: the pin, the code, the tests and the record. If the team ever has to undo it, git revert takes all of it back at once.
Common mistake
The code moved to version 4, the pin did not
Symptom: a colleague ran python -m pip install --upgrade marshmallow in his own environment. He committed the new code, and python -m pytest printed 24 passed for him. Kwame makes a fresh environment from the lock file, and gets:
FAILED tests/test_cli.py::test_cli_writes_accepted_and_rejected - AssertionEr...FAILED tests/test_schema.py::test_page_addresses_from_the_web_form_are_accepted[https://larkfield.example#help]FAILED tests/test_schema.py::test_page_addresses_from_the_web_form_are_accepted[https://caf\xe9.larkfield.example/help]3 failed, 21 passed in 0.03sCause: pip install --upgrade changed one computer, not the project. Both requirements files still say marshmallow==3.26.2, so every fresh environment installs the old version. The new code runs on it, but it rejects Grace’s addresses again.
Fix: an upgrade is a change to the requirements files, in the same commit as the code. Change the lock file, then install from it, never with --upgrade. Kwame’s fresh environment is the check: it must pass too.
Part 9: new input, end to end
Section titled “Part 9: new input, end to end”data/new-submissions.json has 10 submissions that no step has used yet. First, predict each decision from the README’s rules, on paper. Then run it:
python -m form_intake data/new-submissions.json10 submissions: 5 accepted, 5 rejected.Saved: out/accepted.json, out/rejected.jsonShow each decision
| Submission | What is special | Decision |
|---|---|---|
| T-4013 | a normal page address | accepted |
| T-4014 | https://larkfield.example#contact |
accepted: the 4.3.1 fix |
| T-4015 | https://müller.larkfield.example/help |
accepted: the 4.2.4 fix |
| T-4016 | an empty subject | rejected: subject “Invalid value.” |
| T-4017 | quinn.example.com |
rejected: “Not a valid email address.” |
| T-4018 | no category, priority or page | accepted: other, 2, no page |
| T-4019 | 2026-10-10, a date alone |
rejected: “Not a valid datetime.” |
| T-4020 | priority "high" |
rejected: “Not a valid integer.” |
| T-4021 | a normal page address | accepted |
| T-4022 | the year 2062 | rejected: “The time is in the future.” |
If your prediction differs from the output, find out which one is wrong: the rule in the README, your reading of it, or the code. T-4016 and T-4019 test two of the silent changes of Part 2. With 4.3.1 and the old validator or the plain DateTime field, both would be accepted.
Insights and limits
Section titled “Insights and limits”- Read before you install. The 20 warnings and the 4.0.0 changelog predicted every loud error. When the upgrade ran, only one of them showed.
- The silent changes are the real work. Four breaks stopped the program, and three did not. Two of the silent ones had a test, and
orderedis harmless. But the time change was wider than its changelog entry, and only the saved baseline found the rest. - The assistant was right about the changelog. In this recording, all seven reasons matched real entries. It found the hidden breaks from one error message and the code. Its one weak change followed the only evidence that it had. A different run, model or assistant can do better or worse: check every time.
- Limits. One package, one upgrade, one recorded session. The probe has 10 time formats, not every input that a web form can send. Tested on macOS only.
Responsible use
Section titled “Responsible use”- Install what you checked. Install from the lock file, with exact versions. Read the release page on pypi.org before you trust it: who published it, when, and with which licence.
- Keep the assistant inside the task. The prompt allowed two files, and no change to the tests or the requirements. An assistant that edits the tests until they pass has removed the evidence.
- Send the least data. The prompt had test output and code, and no customer data. Never paste real submissions into an assistant. Use invented ones, as here.
- Name the dependency’s licence in the record. A new MAJOR version can change it. marshmallow 4.3.1 is still MIT.
- Say what the assistant did. The decision record names the recorded session, and the change that the team made to it.
Make it a portfolio piece
Section titled “Make it a portfolio piece”Every team must upgrade its dependencies, and nobody sees the work when it goes well. This project shows that you can do it with care. You read first, save the behaviour, use an assistant, and review it with evidence.
What to put in the repository:
form-intake/ README.md what it does, how to run and check it, the rules DECISIONS.md decision 2: why, each change and its changelog entry, the checks, what you changed LICENSE MIT form_intake/ the schema and the command tests/ 24 tests, among them the time formats data/ the three invented input files tools/ compare.py assistant/ the transcript and the patch, with a note on what you kept- Make a new repository from your project folder, with its history:
Start, Grace’s tests, and the upgrade. Push it to GitHub if you have an account; it is not required. - In the README, add a short section, The marshmallow 4 upgrade. Give the comparison table of the four versions, and link
DECISIONS.md. - Check that a fresh clone with a fresh environment from the lock file prints
24 passed, as Kwame’s did.
The other case study of this course is Fix a bug in an unfamiliar codebase.
Knowledge check
Section titled “Knowledge check”Check your understanding
After the assistant's change, all 15 tests passed, also with warnings as errors. Why was that not enough to merge?
Show the answer
The tests check only the rules that someone wrote down for 3.26.2. marshmallow 4 reads times with datetime.fromisoformat. It accepts forms that 3.26.2 rejected, and no test covered them. The saved decisions of Part 3 showed that 6 of the 10 time formats changed their decision. “The tests pass” says that the written rules hold. It does not say that nothing else changed.
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
With 4.3.1 and no code change, pytest showed only one error: 'missing'. How many breaks did the project really have, and how could you know before the upgrade?
Show the answer
Seven: missing (in three fields), the validator that returns False, the data_key argument, context, ordered, __version__, and the time format. Python stops at the first error when a module loads, so one error hides the others, and the silent ones give no error. You could know before the upgrade from three sources. The 20 warnings of 3.26.2 named most of them. pytest -W error::DeprecationWarning showed the first error. And the 4.0.0 changelog, mapped to the code, listed them all.
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
The changelog suggests contextvars or the experimental Context class to replace Schema(context=...). The assistant used a constructor argument instead. Is the change wrong?
Show the answer
No. The changelog says what was removed and suggests one way to replace it; it does not forbid others. SubmissionSchema(now=now) is plain Python. It keeps the behaviour (all tests pass), and it avoids an API that the changelog itself calls experimental. It is a choice, so it goes into DECISIONS.md with its reason. A review checks that a change is correct and explained, not that it copies the guide.
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 new MAJOR version may remove or change things. Read the changelog and the warnings before you install.
- Write the user’s problem as tests first. Save today’s decisions on more inputs than the tests have.
- Upgrade on a branch, in both requirements files, and install from the lock file.
- Give the assistant a bounded task, and ask for a reason for each change. In this recording, all seven reasons matched the changelog.
- Review each change against the changelog and the saved decisions. The assistant’s version passed every test and still changed six decisions. The reviewed one changed three, each on purpose.
- Pin, record and commit the upgrade as one change.
Next, the final assignment: add one feature and repair one defect in a small Python project, with the evidence that this course has taught.
Next: Final assignment