Lock current stock output before you extract any helper. A rewrite that also changes prints will hide regressions. The safe path is a green characterization suite, then one extract.
This note uses a small inventory adjuster as the specimen. The module is a proposal, not a captured production trace. Treat every command below as an unexecuted example.
Why an unlocked extract fails
Messy repos often mix math, mutation, and printing. One function reads a dict, writes a dict, and prints a line. Callers then depend on both the dict and the text.
A later cleanup may sort keys or change spacing. Tests that only check the final dict will stay green. Operators who read logs will still see a broken report.
Public contribution drives often reward large, visible diffs. A large rewrite is a poor first patch on a messy module. A locked extract is easier to review than a full cleanup.
Three locks before any edit
Freeze three observations before you attempt any structural edit. Each lock should fail if the extract changes that observation. Leave product wording for a later, separate commit.
Lock the printed lines in their current order. Lock the final quantity map after every adjustment row. Lock the rejection text when the sku is missing.
| Observation | Assertion style | Keep out of this commit |
|---|---|---|
| Printed order | Exact line list | Sorting and label edits |
| Final quantities | Dict equality | Key renames |
| Missing sku text | Exact reject line | New exception types |
| Dict identity | assertIs if callers need it | Returning a copy |
Use this matrix before you accept a suggested patch. A row marked out must wait for a later contract. A row you keep still needs the same green suite.
Walk four rows before writing asserts
Walk one input before you trust the written locks. Start with sku A at 10 and sku B at 2. Apply the rows in the table below, in that order.
| Step | Call | Map after call | Printed line |
|---|---|---|---|
| 1 | sale A 3 | A=7, B=2 | ok A sale 3 left 7 |
| 2 | recv B 4 | A=7, B=6 | ok B recv 4 left 6 |
| 3 | sale C 1 | A=7, B=6 | reject missing C |
| 4 | hold A 1 | A=7, B=6 | reject kind hold |
Those four rows are the whole contract for this extract. If your code prints different text, lock that text instead. Do not correct the specimen to match this table.
Run this probe from the directory that contains adjuster.py. Compare its printout with the four-row table above. If they differ, trust the probe and rewrite the table.
python - <<'PY'
from io import StringIO
from contextlib import redirect_stdout
from adjuster import adjust_stock
stock = {"A": 10, "B": 2}
buf = StringIO()
with redirect_stdout(buf):
adjust_stock(stock, "A", "sale", 3)
adjust_stock(stock, "B", "recv", 4)
adjust_stock(stock, "C", "sale", 1)
adjust_stock(stock, "A", "hold", 1)
print(buf.getvalue())
print(stock)
PY
Specimen to leave messy
The specimen below is intentionally awkward and mixed. It adjusts stock, prints a line, and rejects unknown codes. Do not clean it before the tests exist.
def adjust_stock(stock, sku, kind, qty):
if sku not in stock:
print("reject missing %s" % sku)
return stock
if kind == "sale":
stock[sku] = stock[sku] - qty
print("ok %s sale %s left %s" % (sku, qty, stock[sku]))
elif kind == "recv":
stock[sku] = stock[sku] + qty
print("ok %s recv %s left %s" % (sku, qty, stock[sku]))
else:
print("reject kind %s" % kind)
return stock
Put the specimen in a scratch package before you test it. Keep unrelated production imports out of this first slice. You want one file you can read without extra context.
mkdir -p stocklab
# save the function as stocklab/adjuster.py
python -m py_compile stocklab/adjuster.py
1. Record the contract in tests
Characterization tests record what the code does today. They are not approval of that behavior as a product rule. Say that difference in the test module docstring.
The test file below is a proposal you can run locally. It uses redirect_stdout so prints do not leak. Save it as test_adjuster.py next to the specimen.
Prefer redirect_stdout instead of a third-party capture plugin. The standard library keeps this lock easy to copy. A snapshot file would hide the contract in another path.
import unittest
from io import StringIO
from contextlib import redirect_stdout
from adjuster import adjust_stock
class AdjustStockContract(unittest.TestCase):
"""Records current lines and totals. Does not approve them."""
def test_lines_map_and_identity(self):
stock = {"A": 10, "B": 2}
buf = StringIO()
with redirect_stdout(buf):
out1 = adjust_stock(stock, "A", "sale", 3)
out2 = adjust_stock(stock, "B", "recv", 4)
out3 = adjust_stock(stock, "C", "sale", 1)
out4 = adjust_stock(stock, "A", "hold", 1)
self.assertIs(out1, stock)
self.assertIs(out2, stock)
self.assertIs(out3, stock)
self.assertIs(out4, stock)
self.assertEqual(
buf.getvalue().splitlines(),
[
"ok A sale 3 left 7",
"ok B recv 4 left 6",
"reject missing C",
"reject kind hold",
],
)
self.assertEqual(stock, {"A": 7, "B": 6})
if __name__ == "__main__":
unittest.main()
2. Run the suite before any edit
Run the suite from the scratch package root. Do not edit the adjuster until this run is green. A red suite means the lock does not match the code.
cd stocklab
python -m unittest test_adjuster.py -v
Fix the expected text, not the production behavior. Update the test only when you misread the current output. Never change the module to satisfy a guessed expectation.
3. Extract only the delta math
Move only the arithmetic into one pure function. Leave print text and dict writes in the original function. Pass the same numbers the old branch already used.
The raise path is not part of the locked contract. Wrong kinds still hit the original reject print. Do not add a test that expects ValueError here.
def apply_delta(current, kind, qty):
if kind == "sale":
return current - qty
if kind == "recv":
return current + qty
raise ValueError("unsupported kind")
def adjust_stock(stock, sku, kind, qty):
if sku not in stock:
print("reject missing %s" % sku)
return stock
if kind == "sale":
stock[sku] = apply_delta(stock[sku], kind, qty)
print("ok %s sale %s left %s" % (sku, qty, stock[sku]))
elif kind == "recv":
stock[sku] = apply_delta(stock[sku], kind, qty)
print("ok %s recv %s left %s" % (sku, qty, stock[sku]))
else:
print("reject kind %s" % kind)
return stock
4. Re-run the same suite
Run the identical characterization suite after the extract. Green means the locked stock lines did not change. Red means the extract altered a locked behavior.
Revert the extract if any locked line differs. Do not patch the test to match a new print. The suite is the contract for this change only.
python -m unittest test_adjuster.py -v
5. Stop after one helper
Do not sort keys in this same change. Do not rename printed labels in this same change. Queue those wording edits as later, separate commits.
Two extracts that look small and fail
Two common extracts break this module in quiet ways. One moves formatting into the helper and drifts a space. One returns a fresh dict and drops caller identity.
The characterization suite should catch both of those failures. A line lock fails when a space or label drifts. A map lock fails when keys or quantities change.
Dict identity is the optional third lock in the suite. Add it only if callers rely on the same object. Use an identity assert, not a second equality check.
Draft tests with a free model, then run them
A free model can draft the first characterization file. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Use that draft as a candidate, then compare it to the module.
Free model access can speed the first draft of that file. This note does not claim a model name, quota, or uptime. Paste the full adjuster, or the draft may invent branches.
Ask the model for tests, not for a redesigned adjuster. Give it the specimen and the three locks listed above. Discard output that adds files you did not request.
Reject any draft that changes the expected totals. Reject any draft that adds a new public behavior. Keep the test focused on lines, the map, and identity.
A free server can run the suite on a clean checkout. Use it when your laptop already holds unrelated dirty files. This note does not claim hardware size or session length.
Copy the same file bytes you already reviewed locally. Run the unittest command before you edit the adjuster. A remote run does not replace reading the diff yourself.
If you have MonkeyCode's free server, run this suite once there. Treat that run as a clean-room check, not as a new requirement. Local review still decides whether the extract may land.
Limits of a green lock
Characterization tests lock current behavior, including accidental bugs. If a total is wrong, the suite will protect that wrong total. Confirm the rule with a human owner before you bless it.
This workflow fits one module with stable, local inputs. It does not fit a service that calls a live network. Clock and remote IO need separate controls before any extract.
A sale larger than stock still prints a negative left. A sale of 100 from a stock of 10 yields a negative left. Do not fix it inside the same extract commit.
The specimen does not validate the type of qty. A non-numeric qty still fails inside the subtraction. Do not add a friendly error in this same commit.
The example uses only the Python standard library. No third-party runner is required to lock these lines. Exact string asserts make spacing part of the contract.
Who should skip this path
Skip this path if you lack a known current output. Skip it when you cannot name a real caller of the function. Skip it if the next change must alter the report format.
A format change is a product edit, not a safe extract. Write the new text contract before you change the printer. Do not hide a format change inside a helper move.
Skip the free-model draft if you cannot review Python tests. A generated file is not evidence that the lock is right. Your green run on the specimen is the evidence.
Close the change
Count the assertions that touch printed text lines. Count the assertions that touch the final quantity map. Both counts should be at least one before you extract.
Read the diff and confirm only a helper was added. Confirm the original function still prints the same labels. Confirm no import, path, or env read was introduced.
Stop the session after one green extract lands. Open a new change if you still want cleaner names. Smaller diffs keep the characterization contract easy to read.
Top comments (0)