Debugging Tools
Python ships with breakpoint() and pdb, integrates with richer REPLs like ipdb, supports remote attach debuggers, and relies on structured logging for production. Pick the tool for local reproduction vs observing live systems.
Debugging Tools Reference
| Tool | Type | Description |
|---|---|---|
breakpoint() | stdlib debugger entry | Calls sys.breakpoints() hook (default pdb.set_trace) |
pdb / pdb.pm() | post-mortem | Inspect stack after exception |
ipdb | enhanced REPL | IPython tab completion and syntax highlighting |
debugpy | remote attach | VS Code/PyCharm attach to running process |
logging | production signal | Structured context without stopping threads |
faulthandler | crash dump | Dump trace on SIGABRT/hang |
tracemalloc | memory | Allocation snapshots (see memory scenarios) |
Recipe
Quick-reference recipe card - copy-paste ready.
def charge(amount: int) -> int:
breakpoint() # Python 3.14: respects PYTHONBREAKPOINT
if amount < 0:
raise ValueError("negative")
return amount * 100
# post-mortem in pytest: pytest --pdb
# logging:
import logging
log = logging.getLogger(__name__)
log.exception("charge failed", extra={"amount": amount})When to reach for this:
- Reproducing a logic bug with local variables intact
- Inspecting stack after an exception in CI
- Attaching to a staging pod without editing code
- Correlating user reports with request logs in production
Working Example
import logging
import sys
from contextlib import contextmanager
# --- structured logging setup ---
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
log = logging.getLogger("billing")
@contextmanager
def request_context(request_id: str):
old = logging.LoggerAdapter(log, {"request_id": request_id})
try:
yield old
finally:
pass
# --- pdb session commands (comment reference for readers) ---
# p variable -> print
# n -> next line
# s -> step into
# w -> where stack
# l -> list source
# c -> continue
def apply_discount(total_cents: int, pct: int) -> int:
if pct < 0 or pct > 100:
log.error("invalid pct", extra={"pct": pct})
raise ValueError("pct out of range")
discounted = total_cents * (100 - pct) // 100
log.info("discounted", extra={"before": total_cents, "after": discounted})
return discounted
def debug_example() -> None:
# conditional breakpoint in dev only
if sys.flags.debug_mode: # or os.getenv("DEBUG_PDB") == "1"
breakpoint()
print(apply_discount(10_00, 10))
# --- post-mortem helper ---
def run_with_pm(fn):
try:
return fn()
except Exception:
import pdb
pdb.post_mortem()
raise
# --- faulthandler for hangs ---
import faulthandler
faulthandler.enable()# pytest with pdb on failure
uv run pytest tests/test_billing.py --pdb -x
# remote debug (debugpy) - start listener in app startup when DEBUG_ATTACH=1
# VS Code attaches to port 5678What this demonstrates:
loggingrecords business values without stopping serving threadsbreakpoint()halts locally; guard with env flag in shared code pathspytest --pdbdrops into post-mortem on assertion failuresfaulthandlerhelps when processes hang without raising
Deep Dive
How It Works
breakpoint()consultsPYTHONBREAKPOINT(defaultpdb.set_trace)- Debuggers run in the same process; breakpoints block that thread or process
debugpylistens on a port; IDE injects breakpoints without redeploy- Logging should be structured in production; pdb is for dev/staging reproduction
pdb Command Shortlist
| Command | Action |
|---|---|
h | help |
n | next |
s | step |
r | return |
until | run until line greater than current |
pp expr | pretty-print |
!stmt | execute Python statement |
Python Notes
# disable breakpoints in prod entrypoint
import os
if os.getenv("ENVIRONMENT") == "production":
os.environ["PYTHONBREAKPOINT"] = "0"Gotchas
- Leaving
breakpoint()in main - stalls production workers. Fix: env guard; ruff can flag debug statements. - Printing secrets in pdb - session history logs sensitive data. Fix: redact; use logging with filters.
- Remote debug open on 0.0.0.0 in prod - RCE risk. Fix: staging-only, SSH tunnel, auth.
printdebugging in hot loops - I/O slower than the bug. Fix: sampled logging at INFO/WARN.- pytest
--pdbin parallel xdist - workers attach confusingly. Fix:-n0when debugging. - Optimizing away variables under
-O-assertand some debug paths stripped. Fix: do not rely on assert for prod validation.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
ipdb | want IPython features | minimal CI images without dep |
pudb | full-screen TUI | headless servers |
sentry stack traces | prod error aggregation | stepping through locals |
OpenTelemetry traces | latency across services | line-level logic bugs |
FAQs
What is the difference between pdb and ipdb?
pdb is stdlib. ipdb wraps it with IPython completion and nicer tracebacks. Install ipdb in dev dependency group only.
How do I use breakpoint() in FastAPI?
Set PYTHONBREAKPOINT=ipdb.set_trace in dev. One worker (uvicorn --reload) avoids multi-process confusion. Never in production images.
When should I use logging instead of pdb?
Production, staging under load, and any defect you cannot reproduce locally. Logs preserve evidence after the process moves on.
How does pytest --pdb work?
On test failure, pytest opens post-mortem at the exception frame. Combine with -x to stop at first failure.
Can I debug asyncio code?
Yes with breakpoint() in coroutines when the loop is running under a single worker. For complex cases use asyncio debug mode (PYTHONASYNCIODEBUG=1).
How do I attach VS Code with debugpy?
Add a guarded debugpy.listen(("127.0.0.1", 5678)) and debugpy.wait_for_client() in staging only; forward port via SSH.
What is PYTHONBREAKPOINT=0?
Disables breakpoint() calls - useful in production entrypoints to prevent accidental commits from halting deploys.
Should I use print in libraries?
No. Libraries log via logging.getLogger(__name__). Applications configure handlers and levels.
How do I log JSON?
Use python-json-logger or structlog in apps; include request_id, user id hash, and error type - not raw passwords.
Does Django 5.2 change debugging?
django-debug-toolbar stays dev-only. Use Django logging config; never expose toolbar in production settings.
How do I debug Celery tasks?
celery worker --loglevel=INFO plus task IDs in logs. Reproduce eagerly with task.apply() in shell before remote attach.
When is faulthandler enough?
When processes hang without exceptions - deadlock or C extension stall. SIGUSR1 can trigger stack dump if configured.
Related
- Mutable Default Argument Bugs - inspect with pdb
- asyncio Deadlocks & Blocking-the-Loop - asyncio debug mode
- Memory Leak Scenarios - tracemalloc companion
- Dependency & Environment Conflicts - verify interpreter in pdb
- Debugging Refactor Snippets - fixes after root cause found
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.