systematic-debugging
verified0428cc7b-97b8-4b2a-9590-bde5cca5774c
Use when debugging any bug — 4-phase root cause methodology: reproduce, isolate, hypothesize, fix-verify. No code changes before a repro exists.
Metadata
Skill file
# Systematic Debugging
**Use when** you encounter a bug of any severity. Never change code before completing Phase 1. This is a four-phase, evidence-first methodology.
## Phase 1: Reproduce (NO CODE CHANGES YET)
Rule: if you cannot reproduce the bug, you cannot fix it.
```bash
# 1. Capture the exact command/input that triggers it
# 2. Record the environment
python -c "import sys, platform; print(sys.version, platform.platform())"
# 3. Confirm it fails every time — run it 5x
for i in $(seq 5); do
pytest tests/test_broken.py::test_thing -x 2>&1 | tail -1
done
```
**Minimum repro** — strip everything possible:
```python
# Before: 200-line script that fails 30% of the time
# After: 8-line repro that fails 100%
def test_minimal_repro():
from app.service import process
assert process({"id": None, "items": []}) == {"status": "ok"}
# Should fail with AttributeError: 'NoneType' object has no attribute 'lower'
```
Decision gate:
| Can you reproduce? | Action |
|---|---|
| Yes, 100% of the time | Proceed to Phase 2 |
| Yes, but only sometimes | Go to flaky-test-triage skill first |
| No, never on your machine | Check env diff: Python version, OS, deps (`pip freeze` diff) |
## Phase 2: Isolate
Narrow from the system to the failing line using bisection.
```python
# Binary-search logging: log at midpoint, run, cut in half
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Then add:
logger.debug("Entering process_order: order_id=%s, items=%s", order_id, len(items))
```
**Git bisect** (if it worked before):
```bash
git bisect start
git bisect bad HEAD
git bisect good v2.5.0
git bisect run pytest tests/test_broken.py::test_thing
git bisect reset
```
**Binary search by commenting**: comment out the back half of the function. If it still fails, the bug is in the front half. Repeat. You'll find the exact line in ~log2(n) steps.
## Phase 3: Hypothesize
Before changing ANYTHING, write down:
1. "I believe the root cause is _____________ because _____________"
2. "If I'm right, changing X to Y will make the test pass"
3. "If I'm wrong, I'll see _____________ instead"
```python
# Test the hypothesis WITHOUT changing production code
def test_hypothesis():
# If root cause is None handling, then passing a valid id should work
result = process({"id": "abc123", "items": []})
assert result["status"] == "ok"
# This should PASS if hypothesis is correct, FAIL if something else is wrong
```
## Phase 4: Fix + Verify
Make exactly ONE minimal change:
```python
# BEFORE (failing):
def process(payload):
normalized = payload["id"].lower() # AttributeError on None
# AFTER (fixed):
def process(payload):
uid = payload.get("id")
normalized = uid.lower() if uid else ""
```
Then:
```bash
# 1. Prove the repro now passes
pytest tests/test_minimal_repro.py -x -v
# 2. Prove nothing else broke
pytest tests/ -x --no-header -q
# 3. If the fix introduced a new edge case, capture it
git diff # review the single change
```
## Guardrails
- NO code changes in Phase 1, 2, or 3. Period.
- One fix. NOT two things at once. You can't tell which one worked.
- "It works now" is not a root cause. You must be able to explain WHY.
- If the fix is >10 lines, you haven't isolated enough.
## Pitfalls
| Pitfall | Consequence | Fix |
|---------|-------------|-----|
| Fixing before reproducing | You're guessing, not debugging | Delete the fix, start at Phase 1 |
| Multiple simultaneous "fixes" | Can't tell which solved it | Revert all, apply one at a time |
| Skipping the hypothesis | Fix symptoms, not causes | Write the hypothesis on paper |
| Not running full suite after | Regression bugs | `pytest tests/ -x` after every fix |
| Accepting "it doesn't repro for me" | Bug lives on in prod | Get exact env from the reporter |
## Verify / Checklist
- [ ] Minimal repro exists and fails 100% (flaky <10%: separate triage)
- [ ] The failing line is identified (not "somewhere in this module")
- [ ] Hypothesis written and tested BEFORE code change
- [ ] Exactly ONE change in the diff
- [ ] Full test suite passes after the fix
- [ ] Root cause can be explained in one sentence
Attached files
No attached files.