Which stage lies?
Most real code is a pipeline: data flows through step after step, and the symptom only shows at the end. To isolate the faulty stage, print the value between every stage and compare each checkpoint against what you expected there. The first checkpoint that looks wrong points at the stage right before it, and everything downstream is innocent.
Two practical touches:
- print with
repr()(or an f-string's{x!r}) so hidden characters show up:'hello 'with a trailing space is invisible without the quotes - label every checkpoint (
after parse:), because five unlabeled lists teach you nothing
This is the experiment step of lesson 7-1's loop, run on all stages at once.
A healthy pipeline with checkpoints
Three stages, each followed by a labeled repr checkpoint, so every stage can be verified independently of the rest.
def strip_ends(s): return s.strip() def lower(s): return s.lower() def kebab(s): return s.replace(" ", "-") raw = " Debug Like A Scientist " step1 = strip_ends(raw) print("after strip:", repr(step1)) step2 = lower(step1) print("after lower:", repr(step2)) step3 = kebab(step2) print("after kebab:", repr(step3))
Output
after strip: 'Debug Like A Scientist' after lower: 'debug like a scientist' after kebab: 'debug-like-a-scientist'
Without the repr calls, the quotes disappear and so does the trailing-space information. Debug Like A Scientist printed bare is identical to Debug Like A Scientist on screen, and that invisible difference is exactly the class of bug checkpoints are meant to expose.
The labels do the other half of the work. Three unlabeled strings in the output would require counting lines to know which stage produced which, and the moment you add a fourth stage the counting goes wrong.
Reading a healthy run first is worth the time, because it establishes what correct looks like at every stage. Debugging a pipeline you have never seen working means guessing at both the symptom and the expectation at once.
Locating the bug from checkpoints
In a four-stage pipeline where stages 1 and 2 look correct and stages 3 and 4 are both wrong, the bug is in stage 3, the first stage whose output disagrees with expectations.
Garbage in means garbage out, so once stage 3 emits a wrong value, stage 4 faithfully transforms garbage into more garbage. The first wrong checkpoint is the crime scene, and everything after it is the bug propagating rather than a second bug.
That asymmetry is why checkpoints isolate so fast. One run of the pipeline eliminates every stage before the first bad checkpoint as innocent and every stage after it as merely downstream, which turns four suspects into one.
Two cautions keep the technique honest. Stage 4 being wrong tells you nothing until stage 3 is fixed, so resist patching it, and it is possible for two stages to be independently broken, which is why you re-run after the fix rather than assuming the pipeline is clean.
Finding the lying stage
This pricing pipeline has one broken stage. Before the fix, apply_discount computed p * (1 - percent).
def parse_prices(text): return [float(p) for p in text.split(",")] def apply_discount(prices, percent): return [round(p * (1 - percent / 100), 2) for p in prices] def add_tax(prices, rate): return [round(p * (1 + rate), 2) for p in prices] raw = "10.00,20.00" step1 = parse_prices(raw) print("after parse:", step1) step2 = apply_discount(step1, 10) print("after discount:", step2) step3 = add_tax(step2, 0.05) print("after tax:", step3)
Output
after parse: [10.0, 20.0] after discount: [9.0, 18.0] after tax: [9.45, 18.9]
With the bug present, the checkpoints read after parse: [10.0, 20.0], which is right, then after discount: [-90.0, -180.0], which is not, so the discount stage is the first liar. The tax stage then dutifully scales those negatives, and its checkpoint is wrong only because its input was.
The cause is a unit mismatch. The percent arrives as 10, meaning ten percent, and 1 - 10 is -9, so the formula needs a fraction rather than a whole percent. Dividing by 100 inside the stage gives p * (1 - percent / 100), which is the same formula you met in lesson 2-1.
The fix touches one stage only, which is the discipline from lesson 7-1 about changing one thing per experiment. Adjusting add_tax as well, to compensate for numbers that are about to become correct, is how a one-line bug turns into a two-line bug.
Worth noticing what would have made this bug harder to find. Without checkpoints, the only visible output is the final list, and a negative total is consistent with a broken parser, a broken discount, or a broken tax stage. The labeled intermediate values are what collapse three hypotheses into one.