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.
Search across all documentation pages
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.
| 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) |
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:
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:
logging records business values without stopping serving threadsbreakpoint() halts locally; guard with env flag in shared code pathspytest --pdb drops into post-mortem on assertion failuresfaulthandler helps when processes hang without raisingbreakpoint() consults PYTHONBREAKPOINT (default pdb.set_trace)debugpy listens on a port; IDE injects breakpoints without redeploy| 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 |
# disable breakpoints in prod entrypoint
import os
if os.getenv("ENVIRONMENT") == "production":
os.environ["PYTHONBREAKPOINT"] = "0"breakpoint() in main - stalls production workers. Fix: env guard; ruff can flag debug statements.print debugging in hot loops - I/O slower than the bug. Fix: sampled logging at INFO/WARN.--pdb in parallel xdist - workers attach confusingly. Fix: -n0 when debugging.-O - assert and some debug paths stripped. Fix: do not rely on assert for prod validation.| 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 |
pdb is stdlib. ipdb wraps it with IPython completion and nicer tracebacks. Install ipdb in dev dependency group only.
Set PYTHONBREAKPOINT=ipdb.set_trace in dev. One worker (uvicorn --reload) avoids multi-process confusion. Never in production images.
Production, staging under load, and any defect you cannot reproduce locally. Logs preserve evidence after the process moves on.
On test failure, pytest opens post-mortem at the exception frame. Combine with -x to stop at first failure.
Yes with breakpoint() in coroutines when the loop is running under a single worker. For complex cases use asyncio debug mode (PYTHONASYNCIODEBUG=1).
Add a guarded debugpy.listen(("127.0.0.1", 5678)) and debugpy.wait_for_client() in staging only; forward port via SSH.
Disables breakpoint() calls - useful in production entrypoints to prevent accidental commits from halting deploys.
No. Libraries log via logging.getLogger(__name__). Applications configure handlers and levels.
Use python-json-logger or structlog in apps; include request_id, user id hash, and error type - not raw passwords.
django-debug-toolbar stays dev-only. Use Django logging config; never expose toolbar in production settings.
celery worker --loglevel=INFO plus task IDs in logs. Reproduce eagerly with task.apply() in shell before remote attach.
When processes hang without exceptions - deadlock or C extension stall. SIGUSR1 can trigger stack dump if configured.
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+.
Reviewed by Chris St. John·Last updated Jul 19, 2026