The workshop opens on a staged failure, because a visible row count teaches faster than a definition. On the projector, a fake ledger shows three captures for one invoice after a short reply suggested a three-try loop around a gateway timeout. The teaching assistant does not start a lecture. She slides a red pencil across the bench and holds the next patch until the pencil mark and the test log agree.
That hour is the whole lesson. It is aimed at people who already write small Python services and who review a partner's patch before it lands. A fluent answer counts as a draft. A draft earns a mark only after a local test writes evidence a neighbor can rerun on a clean machine.
The pencil is not a stand-in for taste. It is a script exit line the pair reads aloud before anyone talks about merging. If the line and the test disagree, the paragraph loses.
The shop picture is a traveler card, not a courtroom. On a metal part, red pencil means a second person stood at the gauge and marked the card. The mark is worthless if the gauge never moved. Here the gauge is a unit test that counts capture attempts, and the mark is one stamped line.
Eighty minutes, one process
Eighty minutes is enough when the files already sit on the instructor machine. The first ten minutes replay the double charge, with the attempt list on screen, so the failure is a row count. The next fifteen minutes are local setup: a virtual environment, the modules below, and one command that prints three attempts for the bad policy.
The following twenty minutes are the first exercise. Pairs write a charge function that records an invoice key before the call and refuses a second call in the same process, even when the first call raised a timeout. They are not designing a payment network. They are stopping one process from repeating a side effect whose result is unknown.
Fifteen minutes go to the countercheck. A neighbor may paste a model reply into a text file, but only that neighbor runs the stamp, and the stamp does not read confidence. The last twenty minutes are a clean-directory rerun, including a cheat where someone types a passing line by hand. The neighbor deletes that file, runs the test, and makes the pencil follow the gauge.
Files the room can rerun
The modules are a lab design, written to be copied and executed. They are not a transcript of a timed class and they are not a measured production incident. ledger.py appends the attempt before it decides to time out. That order is the trap.
A retry loop can sound careful and still leave three rows, because the side effect happens before the exception. Students replace the loop with charge_once. The seen set lives only in memory, and that weakness is part of the lesson. A later week can put the same key in a journal file. This week stops at one process.
class Ledger:
def __init__(self):
self.attempts = []
def capture(self, invoice_id, cents):
self.attempts.append((invoice_id, cents))
if str(invoice_id).endswith("-flaky"):
raise TimeoutError("gateway timeout")
return {"invoice_id": invoice_id, "cents": cents, "status": "captured"}
def charge_once(ledger, invoice_id, cents, seen):
if invoice_id in seen:
return {
"invoice_id": invoice_id,
"status": "already-attempted",
"attempts": len(ledger.attempts),
}
seen.add(invoice_id)
try:
return ledger.capture(invoice_id, cents)
except TimeoutError:
return {
"invoice_id": invoice_id,
"status": "unknown-do-not-retry",
"attempts": len(ledger.attempts),
}
def charge_with_blind_retry(ledger, invoice_id, cents, _seen):
last = None
for _ in range(3):
try:
return ledger.capture(invoice_id, cents)
except TimeoutError as exc:
last = exc
raise last
demo_blind.py is the opening scene reduced to a command. It does not pass or fail a suite. It prints the row count, and a correct copy prints blind attempts=3. If a pair hides the failure by catching the error inside capture, the count drops and the instructor puts the append back. The bad behavior has to stay visible, or the pencil has nothing to contradict.
from ledger import Ledger, charge_with_blind_retry
def main():
ledger = Ledger()
try:
charge_with_blind_retry(ledger, "inv-9-flaky", 1250, set())
except TimeoutError:
pass
print("blind attempts=%s" % len(ledger.attempts))
if __name__ == "__main__":
main()
The gauge does not fail the suite in order to scold the specimen loop. It proves the once-only policy and writes one evidence line. The specimen stays in the other file so the demo command can still show three rows after the suite is green. A room that starts from a red bar learns to chase the first assertion. This room learns to produce a line a second script can read.
import unittest
from pathlib import Path
from ledger import Ledger, charge_once
EVIDENCE = Path("evidence.txt")
class RedPencilTest(unittest.TestCase):
def test_timeout_does_not_authorize_a_second_capture(self):
ledger = Ledger()
seen = set()
result = charge_once(ledger, "inv-9-flaky", 1250, seen)
self.assertEqual(result["status"], "unknown-do-not-retry")
self.assertEqual(ledger.attempts, [("inv-9-flaky", 1250)])
self.assertEqual(result["attempts"], 1)
EVIDENCE.write_text("PASS once-only attempts=1\n", encoding="utf-8")
def test_same_key_is_not_sent_again(self):
ledger = Ledger()
seen = set()
charge_once(ledger, "inv-9-flaky", 1250, seen)
again = charge_once(ledger, "inv-9-flaky", 1250, seen)
self.assertEqual(again["status"], "already-attempted")
self.assertEqual(len(ledger.attempts), 1)
if __name__ == "__main__":
unittest.main()
stamp.py lowercases the reply and rejects a few workshop phrases. The phrases are the ones a helpful draft tends to offer: always retry, safe to retry, ignore the test, trust this. The script then requires the evidence file to start with PASS and to contain attempts=1. A pair that wants the stamp rewrites the reply until it describes the once-only stop, then reruns the test so the evidence is fresh.
The script does not compare file clocks. The neighbor does. That human step is deliberate. The lab is a review habit, not a trust root, and a clock check would hide the social part of the pencil.
import sys
from pathlib import Path
BANNED = (
"always retry",
"safe to retry",
"ignore the test",
"trust this",
)
def stamp(reply_path, evidence_path):
reply = Path(reply_path).read_text(encoding="utf-8").lower()
evidence_file = Path(evidence_path)
if evidence_file.exists():
evidence = evidence_file.read_text(encoding="utf-8")
else:
evidence = ""
for phrase in BANNED:
if phrase in reply:
return "REJECT: reply tells the class to retry or to skip tests"
if not evidence.startswith("PASS"):
return "REJECT: no local PASS evidence"
if "attempts=1" not in evidence:
return "REJECT: evidence does not record a single attempt"
return "STAMP: red-pencil-ok"
if __name__ == "__main__":
if len(sys.argv) != 3:
print("usage: python stamp.py reply.txt evidence.txt")
raise SystemExit(2)
print(stamp(sys.argv[1], sys.argv[2]))
A clean pass uses ordinary commands. The lab assumes Python 3 with the standard library and nothing else. No third-party package is required, which keeps the first fifteen minutes from turning into an installer debate.
python -m venv .venv
source .venv/bin/activate
python demo_blind.py
python test_red_pencil.py
printf '%s\n' "charge_once records the key, then stops after a timeout" > reply.txt
python stamp.py reply.txt evidence.txt
The expected stamp line is STAMP: red-pencil-ok. The counterexample should run first. A reply that says it is safe to retry on timeout must print a REJECT line even when evidence.txt already contains PASS. Windows machines can activate with .venv\Scripts\activate and then use the same Python commands. The lesson does not depend on bash.
A draft is not a stamp
Only the second exercise needs a hosted model, and it still works without one. One student may paste the timeout text and ask for a patch. The partner saves that answer, unchanged, as reply.txt, and runs the stamp before anyone applies a diff. A local file that already contains the bad paragraph teaches the same refusal when the network is down. The model supplies drafts. The test supplies the stamp.
Free model access is a practical fit when a department will not issue paid keys for one afternoon, and a free server option is a practical fit when the instructor wants one shared copy of these files instead of a handful of zip attachments. MonkeyCode is one place those two options are offered for this kind of lab. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Neither option is required for the stamp to mean anything.
Terms should be read on the morning of the class, in the project documentation, because availability language moves. This article does not pin a token ceiling, a machine shape, a region, or an end date. A workshop sheet that freezes an unverified number becomes the outdated handout the next cohort has to unlearn.
The cheat stays on the clock because someone will find it. Hand-typing PASS into evidence.txt makes a careless reading look finished. The workshop names that move in the same tone as a skipped gauge. The neighbor deletes the file, runs the test module, and only then runs the stamp script.
If the script and the test disagree, the test wins. A screenshot of the stamp without that rerun is decoration. The pencil is there to slow the conversation, not to decorate the slide.
Who should leave this lab on the bench
This lab should not be pointed at a real charge path, a mail sender, or an inventory decrement. The in-memory set dies with the process, so two workers can still capture the same invoice. It should not be the only review on a pull request. It is a poor first week for students who have not met exceptions, because the timeout will hide the attempt list.
It is also the wrong artifact for anyone comparing model vendors. No latency, cost, quality, or uptime figure is collected. A team that needs those numbers should measure its own traffic against its own success criteria, rather than borrow a classroom stamp and treat it as a score.
A later hour can append the invoice key to a journal and refuse capture when the line already exists. That problem is closer to crash recovery than to reply hygiene, and it needs its own timer. This workshop ends when every pair can show three attempts, then one attempt, and can refuse a smooth paragraph that disagrees with the evidence file.
Instructors who want the fixture on a no-cost shared box can check the current free-server note in the MonkeyCode project before they book the date. If that note no longer matches the plan, the same files stay on the laptops and the stamp does not change.
Top comments (0)