Debug py
Skill spotcircuit/rebar/.claude/skills/software-development/debug-py
Debug Python interactively via pdb (`breakpoint()`, `python -m pdb`, post-mortem) or remote-attach via debugpy/remote-pdb. Use when print/logging is insufficient — set breakpoints, step, walk the stack, dump locals, evaluate expressions, attach to a running daemon, or post-mortem an exception. Complements superpowers:systematic-debugging (which is the general process; this is the Python-specific tooling reference).From its SKILL.md
npx -y skills add spotcircuit/rebar --skill debug-pyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.2 KB, as published. Nobody here has run it
debug-py
When to use
- A test fails and the traceback doesn't reveal why a value is wrong.
- You need to step through a function and watch a collection mutate.
- A long-running process (worker, daemon) misbehaves and can't be restarted.
- Post-mortem: an exception fired and you want the locals at the crash site.
- A subprocess / child worker is the actual bug site.
Don't use for: anything print() / logging.debug / pytest -vv --tb=long --showlocals already reveals.
Related skills
superpowers:systematic-debugging— the general debugging process. This skill is the Python-side tooling.debug-node— Node.js equivalent.
Pick one
| Tool | When |
|---|---|
breakpoint() + pdb | Local, interactive, simplest. Edit the source, run normally. |
python -m pdb script.py | Launch under pdb with no source edits. |
debugpy | Remote / headless / attach-to-running. Talks DAP. |
remote-pdb | Cleanest agent-friendly remote pdb over a TCP socket. Usually what you want from a terminal. |
Start with breakpoint().
pdb reference
Inside (Pdb):
| Command | Action |
|---|---|
h / h cmd | help |
n | next line (step over) |
s | step into |
r | return from current function |
c | continue |
unt N | continue until line N |
j N | jump to line N (same function only) |
l / ll | list source / full function |
w | where (stack trace) |
u / d | move up / down stack |
a | print args of current function |
p expr / pp expr | print / pretty-print |
display expr | auto-print on every stop |
b file:line | breakpoint |
b func | break on function entry |
b file:line, cond | conditional breakpoint |
cl N | clear breakpoint N |
tbreak file:line | one-shot breakpoint |
!stmt | execute arbitrary Python (assignments included) |
interact | drop into full Python REPL in current scope (Ctrl+D to exit) |
q | quit |
interact is the most powerful: import anything, call mutating methods, inspect deeply.
Recipe 1: local breakpoint
def compute(x, y):
result = some_helper(x)
breakpoint() # drops into pdb
return result + y
Run normally. Strip before committing:
rg -n 'breakpoint\(\)' --type py
Recipe 2: launch a script under pdb
python -m pdb path/to/script.py arg1 arg2
(Pdb) b path/to/script.py:42
(Pdb) c
Recipe 3: pytest
# Drop to pdb on failure
pytest tests/path/to/test_file.py::test_name --pdb
# Drop to pdb at start of test
pytest tests/path/to/test_file.py::test_name --trace
# Show locals in tracebacks without pdb
pytest tests/path/to/test_file.py --showlocals --tb=long
pdb does NOT work under pytest-xdist. If your suite runs -n auto, add -p no:xdist or -n 0:
pytest tests/foo_test.py::test_bar --pdb -p no:xdist
Recipe 4: post-mortem on any exception
import pdb, sys
try:
run_the_thing()
except Exception:
pdb.post_mortem(sys.exc_info()[2])
Or wrap a whole script:
python -m pdb -c continue script.py
# On crash, pdb lands at the exception frame
Global hook (REPL/Jupyter):
import sys
def excepthook(etype, value, tb):
import pdb; pdb.post_mortem(tb)
sys.excepthook = excepthook
Recipe 5: remote debug with debugpy
For long-lived processes you can't restart cleanly.
Setup
pip install debugpy
Pattern A — source-edit, wait at launch
import debugpy
debugpy.listen(("127.0.0.1", 5678))
print("debugpy listening on 5678, waiting for client...", flush=True)
debugpy.wait_for_client()
debugpy.breakpoint() # optional: pause once attached
Pattern B — no source edit, launch with -m debugpy
python -m debugpy --listen 127.0.0.1:5678 --wait-for-client your_script.py arg1
python -m debugpy --listen 127.0.0.1:5678 --wait-for-client -m your.module
Pattern C — attach to a running PID
python -m debugpy --listen 127.0.0.1:5678 --pid <pid>
Hardened kernels block ptrace injection. Workaround:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Connecting
VS Code / Cursor / Zed launch.json:
{
"name": "Attach Python",
"type": "debugpy",
"request": "attach",
"connect": { "host": "127.0.0.1", "port": 5678 },
"justMyCode": false
}
Recipe 6: remote-pdb (agent-friendly)
When you want a real (Pdb) prompt over TCP, no DAP overhead:
pip install remote-pdb
from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444) # blocks until connection
Connect from another terminal:
nc 127.0.0.1 4444
For terminal-only debugging from inside a coding agent, prefer remote-pdb over debugpy.
Common pitfalls
- pdb under pytest-xdist silently does nothing. Always use
-p no:xdistor-n 0. breakpoint()in CI / non-TTY hangs the process. Never commit it. Add a pre-commit grep.PYTHONBREAKPOINT=0disables allbreakpoint()calls.echo $PYTHONBREAKPOINTif your breakpoint isn't hitting.debugpy.listendoesn't block unless you also callwait_for_client(). Without it, your first breakpoint may fire before the client attaches.- PID attach fails on hardened kernels.
ptrace_scope=1(Ubuntu default) blocks cross-process ptrace. Either lower it or launch underdebugpyfrom the start. - Threads.
pdbonly debugs the current thread. For multithreaded code, usedebugpyorthreading.settrace()per thread. - asyncio.
pdbworks in coroutines, butawaitinside pdb requires Python 3.13+ — on 3.11/3.12 use!stmt-based awaits viaasyncio.ensure_future. - Forking / multiprocessing. pdb does not follow forks. Each child needs its own
breakpoint()/set_trace().
Verification checklist
-
python -c "import debugpy; print(debugpy.__version__)"works after install. - Port is listening:
ss -tlnp | grep 5678. - First breakpoint actually hits (else:
PYTHONBREAKPOINT=0, xdist, or finished before attach). -
where/wshows the expected stack. - No stray
breakpoint()/set_trace()left committed:rg -n 'breakpoint\(\)|set_trace\(|debugpy\.listen' --type py
One-shot recipes
"Why is this dict missing a key?"
breakpoint()
# (Pdb)
pp d
pp list(d.keys())
w # how did we get here
"Test passes in isolation, fails in the suite."
pytest tests/the_test.py --pdb -p no:xdist
# If only fails alongside others:
pytest tests/ -x --pdb -p no:xdist
"Async handler deadlocks."
import remote_pdb; remote_pdb.set_trace(host="127.0.0.1", port=4444)
Trigger the handler, then nc 127.0.0.1 4444, then w and !import asyncio; asyncio.all_tasks().
"Post-mortem a subprocess crash."
PYTHONFAULTHANDLER=1 python -m pdb -c continue path/to/entrypoint.py