Updates broken pytest tests after intentional code changes — triaging assertion failures from mock-coupling failures from genuine regressions, using Python's introspection to automate where safe. Use when a refactor or API change leaves a pile of failing tests and you need to decide update vs. fix vs. delete.
77
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Same triage discipline as → java-test-updater. Python differences: no compile-time breaks (everything fails at runtime), more mocker.patch coupling, and snapshot libraries make some updates one command.
No compile step means everything surfaces at test runtime:
| Failure | Python-specific signal | Action |
|---|---|---|
AttributeError: 'X' has no attribute 'foo' | Renamed/removed method | Update call site |
TypeError: f() missing 1 positional argument | Signature changed | Add arg or use default |
TypeError: f() got an unexpected keyword | Kwarg renamed/removed | Update kwarg |
ImportError / ModuleNotFoundError | Module moved | Update import |
AssertionError with value diff | Behavior changed (intentional?) or regression | Triage |
AssertionError: Expected 'mock' to be called | Over-mocked internal | Loosen/delete mock |
AssertionError in snapshot compare | Snapshot stale | Review diff, --snapshot-update if intentional |
Python's runtime errors point straight at the problem. For signature changes across many tests:
# conftest.py — one-time shim during migration
# OLD: Order(items, region) NEW: Order(items, region, currency="USD")
# Tests still call Order(items, region). Temporary compat:
@pytest.fixture(autouse=True)
def _order_compat(mocker):
orig_init = Order.__init__
def compat_init(self, items, region, currency="USD"):
orig_init(self, items, region, currency)
mocker.patch.object(Order, "__init__", compat_init)This is a bridge, not a fix. All tests pass → remove the shim → fix tests one by one as you touch them. Don't leave compat shims in permanently.
mocker.patch problemdef test_process(mocker):
mock_validate = mocker.patch("orders.service._validate_order")
mock_save = mocker.patch("orders.service._save_order")
process(order)
mock_validate.assert_called_once_with(order)
mock_save.assert_called_once()_validate_order was inlined into process. Test fails: Expected '_validate_order' to have been called once. Called 0 times.
The behavior didn't change — the order is still validated — but the test was asserting structure. Delete the mock assertion. The test should assert the outcome of validation:
def test_process_rejects_invalid():
bad = Order(items=[])
with pytest.raises(InvalidOrder, match="empty"):
process(bad)This survives refactors because it tests what validation does, not that a function named _validate_order was called.
If using syrupy / pytest-snapshot:
pytest --snapshot-updateThis updates all failing snapshots to current output. Dangerous if any failure is a regression. Workflow:
--snapshot-update. Read every diff.--snapshot-update.assert invoice.total == Decimal("27.80") # fails: actual 27.55git log -p -- src/pricing.py → "Fix: half-even rounding." Intentional. Update with reason:
# abc123: half-even rounding fix. Was 27.80 (half-up bug).
assert invoice.total == Decimal("27.55")Versus:
assert len(results) == 5 # fails: actual 4The change was to a completely different module. Why are there fewer results? Regression. Don't update. Investigate.
pytest.approx driftassert score == 0.8472819 # now fails: 0.8472820Last digit changed — floating-point ops reordered. If precision to 7 decimals isn't spec'd, this was over-tight:
assert score == pytest.approx(0.8473, rel=1e-4)This isn't "loosening to make it pass." It's fixing an over-specific assertion that should never have been that tight.
--snapshot-update. Review diffs first. A regression in a snapshot looks identical to an intentional change.mocker.patch("module._private") failures by updating the patch path. You're chasing implementation. Replace with behavioral assertions.pytest.approx tolerance until the test passes. If rel=0.5 is what it takes, the test isn't testing anything.conftest.py after the migration. They mask further API drift.## Failing tests
Total: <N> Import/Attribute: <N> Signature: <N> Assertion: <N> Mock: <N> Snapshot: <N>
## Mechanical fixes
| Test | Error | Fix |
| ---- | ----- | --- |
## Mock decoupling
| Test | Over-coupled patch | Replacement assertion |
| ---- | ------------------ | --------------------- |
## Assertion triage
| Test | Old | New | Cause commit | Classification | Action |
| ---- | --- | --- | ------------ | -------------- | ------ |
## Snapshot review
| Snapshot | Diff summary | Intentional? |
| -------- | ------------ | ------------ |
## Regressions
<tests correctly failing — file bugs, don't update>
## After
Passing: <N> Updated: <N> Decoupled: <N> Deleted: <N> Bugs filed: <N>47d56bb
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.