Shrink Your Repro Before Debugging
Your debugging is slow because your repro loop is slow. Fix that first. Every hypothesis you test — every print, every breakpoint, every "what if I flip this flag" — runs against your reproduction. If reproducing the bug takes 90 seconds and sometimes doesn't fail at all, you've quietly doubled the cost of every idea you'll have for the next hour. Speed and stabilize the loop before you start theorizing, and every fix reads true.
The loop is the bottleneck
Debugging is a hypothesis–test loop. You form a guess about the cause, run the repro, read the result, and revise. The math is unforgiving: at 90 seconds per run you get about 40 iterations in an hour. At 2 seconds you get hundreds. Same brain, same bug, 10x the shots on goal.
Flakiness is worse than slowness. A repro that fails 70% of the time turns every green run into noise — you can't tell "my fix worked" from "the coin landed heads." You end up re-running to build confidence, which multiplies an already-slow loop.
Three moves to a fast, deterministic loop
1. Shrink to the smallest failing input. A bug that reproduces on a 900-line file, 95 user actions, or a full production payload is a bug you're debugging through a fog. Cut the input in half, re-run, keep whichever half still fails, repeat. This is delta debugging (Zeller, 1999): binary-partition the input and discard everything that isn't load-bearing. In Zeller's original case study, 896 lines of HTML that crashed Mozilla reduced to the single failing line, and 95 user actions to 3.
2. Run the one failing test. Stop running the whole suite. Most runners have a fast path:
| Goal | pytest | jest / vitest |
|---|---|---|
| Stop at first failure | pytest -x | --bail |
| Re-run only what failed last time | pytest --lf | jest --onlyFailures |
| One test by name | pytest -k "name" | -t "name" |
3. Pin seeds and clocks. Non-determinism is the usual reason a repro flakes. Remove the two biggest sources: randomness and time. Seed every PRNG to a fixed value, and freeze the clock so now() returns a constant.
import random
random.seed(1337) # reproducible randomness
from freezegun import freeze_time
@freeze_time("2026-01-01 12:00:00")
def test_expiry():
... # every now() is frozen
Do all three and a 90-second flaky loop becomes a 2-second one that fails the same way every time. Now a passing run means something.
Power moves
- Randomize order to expose the flake, then pin the seed.
pytest-randomlyshuffles test order every run and prints the seed it used. When a run flakes, re-run with-p randomly --randomly-seed=<that seed>to replay the exact order. If the bug only appears under one ordering, you have shared state between tests. - Let the machine minimize for you. For inputs too big to bisect by hand, reach for automated reducers —
git bisectto find the commit, C-Reduce /shrinkrayfor source files, or a property-testing library's built-in shrinker (Hypothesis, fast-check) that hands you the minimal counter-example for free. - Capture the repro as a test. Once you've shrunk it, write it down as a failing test. It paces this session and becomes the regression guard that proves the fix and stops the bug from coming back.
Resources
- Delta debugging (overview) — Wikipedia
- Zeller & Hildebrandt, "Simplifying and Isolating Failure-Inducing Input" (the ddmin algorithm, PDF)
- pytest — stopping after the first failures (
-x,--lf) - freezegun — freeze
datetime.now()in tests - pytest-randomly — seeded, replayable test ordering
<div class="cta"> <strong>Shipping AI features on a slow, flaky loop?</strong> Yeda AI designs, audits, and hardens production LLM systems — including the test harnesses that keep them honest. </div>