Split a god module only after tests seal current behavior. Pin files, clocks, prints, and errors before any move. Then extract one pure function and stop there.
Why unsealed splits drift
A wide module hides parse, math, and disk writes together. A quick extract often changes paths, times, or blank lines. Those diffs look tiny and still break callers.
Reviewers cannot separate a safe move from a contract change. Characterization tests record what the code does today. They do not claim that behavior is the right product.
The only change this pass allows
This pass may move one function boundary in one file. Inputs stay identical, and output bytes stay stable. Clocks, prints, and file writes remain in the caller.
Do not rename public functions in the same commit. Do not add a dependency to clean up formatting. Do not fix a known bug until the seal is green.
What to pin, leave, or block
Use this table before you touch the module. A blocked row means the split is not the smallest safe change.
| Observed signal | Pin now | Leave alone | Block the split |
|---|---|---|---|
| Summary file bytes | Yes | ||
| Printed status line | Yes | ||
| Return value | Yes | ||
| Exception type on a bad row | Yes | ||
| File absent after a bad row | Yes | ||
| Helper names inside the file | Yes | ||
| Comment text | Yes | ||
| New third-party package | Yes | ||
| Clock read moved into the helper | Yes | ||
| Path built from cwd inside the helper | Yes |
Step 1: Keep the messy module unchanged
Treat the file below as the current production shape. It is a proposed example, not a measured repo result. Do not improve it before the tests exist.
import datetime
from pathlib import Path
def run_job(source_text, dest_path):
rows = []
for line in source_text.splitlines():
if not line.strip():
continue
name, qty, price = line.split(",")
rows.append((name.strip(), int(qty), float(price)))
total = 0.0
lines = []
for name, qty, price in rows:
amount = qty * price
total += amount
lines.append(f"{name}:{amount:.2f}")
stamp = datetime.datetime.now().strftime("%Y-%m-%d")
body = "\n".join(lines + [f"TOTAL:{total:.2f}", stamp])
Path(dest_path).write_text(body + "\n", encoding="utf-8")
print(f"wrote {len(rows)} rows")
return total
Three side effects sit beside the row math. The clock is local and the print uses stdout. The write uses only the destination path given.
Step 2: Seal the happy path
Put the test next to the module in a fresh tree. Freeze the date so a laptop clock cannot change the file. Assert bytes, return value, and the exact print.
import datetime as dt
import job_report
class FrozenDate(dt.datetime):
@classmethod
def now(cls, tz=None):
return cls(2026, 10, 9)
def _freeze(monkeypatch):
monkeypatch.setattr(job_report.datetime, "datetime", FrozenDate)
def test_happy_path_seals_bytes_and_print(tmp_path, capsys, monkeypatch):
_freeze(monkeypatch)
dest = tmp_path / "out.txt"
source = "Ada, 2, 3.5\n\nBea, 1, 4\n"
total = job_report.run_job(source, dest)
assert total == 11.0
assert capsys.readouterr().out == "wrote 2 rows\n"
assert dest.read_text(encoding="utf-8") == (
"Ada:7.00\nBea:4.00\nTOTAL:11.00\n2026-10-09\n"
)
The fixture date is a lock, not a news claim. The token 2026-10-09 is the review date for this draft. Change it only if your sealed sample used another day.
Step 3: Seal the empty path and the bad row
Empty input still writes a total line and a stamp. A bad quantity raises ValueError before any write. Import pytest before you add the two tests below.
import pytest
def test_blank_lines_only_write_zero_total(tmp_path, capsys, monkeypatch):
_freeze(monkeypatch)
dest = tmp_path / "out.txt"
total = job_report.run_job("\n \n", dest)
assert total == 0.0
assert capsys.readouterr().out == "wrote 0 rows\n"
assert dest.read_text(encoding="utf-8") == "TOTAL:0.00\n2026-10-09\n"
def test_bad_qty_raises_and_skips_the_file(tmp_path, monkeypatch):
_freeze(monkeypatch)
dest = tmp_path / "out.txt"
with pytest.raises(ValueError):
job_report.run_job("Ada, x, 3\n", dest)
assert dest.exists() is False
Both facts are current behavior, even if a later ticket changes them. Do not assert a friendlier error message in this pass. Record the exception type the module raises today.
Step 4: Run the suite on a clean tree
Run these commands from a new virtual environment. They are the proposed workflow, not a published benchmark. Stop if the seal is red, and do not edit production code yet.
python -m venv .venv
source .venv/bin/activate
python -m pip install pytest
python -m pytest tests/test_job_report.py -q
On Windows, use the Scripts activate script instead. Keep the same pytest invocation after that activation. A green run means the seal matches this tree only.
Commit the tests alone if history allows a test-only commit. The next commit may contain only the extract. Two commits make a reverted move easy to see.
Step 5: Extract one pure function
After the seal is green, move only the row math. Leave parsing, the clock, the print, and the write in run_job. The public signature stays run_job with source text and dest path.
def summarize_rows(rows):
total = 0.0
lines = []
for name, qty, price in rows:
amount = qty * price
total += amount
lines.append(f"{name}:{amount:.2f}")
return lines, total
def run_job(source_text, dest_path):
rows = []
for line in source_text.splitlines():
if not line.strip():
continue
name, qty, price = line.split(",")
rows.append((name.strip(), int(qty), float(price)))
lines, total = summarize_rows(rows)
stamp = datetime.datetime.now().strftime("%Y-%m-%d")
body = "\n".join(lines + [f"TOTAL:{total:.2f}", stamp])
Path(dest_path).write_text(body + "\n", encoding="utf-8")
print(f"wrote {len(rows)} rows")
return total
Re-run the same pytest command with no test edits. A failure means the extract changed a sealed output. Revert the helper and try a smaller move.
Step 6: Reject diffs that fail the table
Read the diff before you trust the green suite. A green suite can miss a new import or a moved clock. Use the block column as a hard stop.
Reject the change if summarize_rows calls datetime or print. Reject it if the helper opens dest_path or reads the working directory. Reject a format tweak, even when the tests still pass by luck.
Add a direct unit check only after the seal stays green. That check may call summarize_rows with in-memory tuples. It must not replace the file and print seals.
def test_summarize_rows_is_pure_math():
lines, total = job_report.summarize_rows([("Ada", 2, 3.5)])
assert lines == ["Ada:7.00"]
assert total == 7.0
This extra test is optional and stays local. Skip it if the extract is not in the tree yet. Do not let it become the only proof.
Where draft help and a remote run fit
A human still owns every sealed assertion here. A model can propose extra cases from the current file. Accept a case only when it matches observed output.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access can draft those candidate cases. Its free server option can run the same pytest command.
That split keeps a laptop clock and a home path out of the baseline. Do not treat a drafted assertion as a specification. Compare each expected string with a local run first.
If the server path differs, keep tmp_path and avoid a home directory. Check MonkeyCode's free server option if a remote baseline helps.
Limits of this seal
The seal preserves bugs as well as useful contracts. A wrong total stays wrong until a later separate change. Say that clearly in the pull request body.
Patching datetime depends on the import style in the module. This example patches import datetime, as written above. A from-import style needs a different patch target.
These tests do not cover concurrent writers or partial disk failures. They also skip encoding errors from the destination filesystem. Add those only if production already depends on them.
A free model draft may fix the bad-row path while sealing it. That is a failed draft, not a better refactor. Delete any assertion the current module does not already satisfy.
Two-decimal formatting can hide binary float noise later. Pick sample prices that land on exact tenths.
A hosted runner is not a permanence promise or hardware claim. Confirm current access in the product before you rely on it.
Who should not use this pass
Skip this pass if the ticket requires a behavior change now. Write the new contract test first, then change code. Do not freeze a bug you must remove in the same commit.
Skip it when the module calls production networks or reads real secrets. A characterization run would repeat that side effect. Isolate or replace that call before you record outputs.
Skip it if nobody can run the suite before merge. An unread model draft is not a seal. Also skip it when the next change must cross several packages at once.
Close on the commit boundary
Green tests plus a one-function diff is the exit bar. If the diff also moves the clock, the pass is not done. Revert the diff, reseal the outputs, and extract less.
Top comments (0)