Debugging and Tooling

This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Beyond print(), Python ships an interactive debugger and profilers in the standard library, and the ecosystem has settled on a small set of linters and formatters for keeping code consistent.

Debugging with pdb

breakpoint() and stepping commands

Calling the built-in breakpoint() drops into the Python Debugger (pdb) at that exact line, documented at the pdb module:

def total(items: list[int]) -> int:
    result = 0
    for item in items:
        breakpoint()          # pauses here on the first iteration
        result += item
    return result


total([1, 2, 3])

Running the script drops into the (Pdb) prompt, where a handful of one-letter commands drive execution:

$ python3 sums.py
> sums.py(5)total()
-> result += item
(Pdb) p item
1
(Pdb) n
> sums.py(3)total()
-> for item in items:
(Pdb) p result
1
(Pdb) c

The core commands are n (next — run the current line, step over calls), s (step — step into a function call), c (continue — run until the next breakpoint), p <expr> (print an expression), and l (list — show source around the current line). q quits the debugger entirely.

PYTHONBREAKPOINT=0 python3 sums.py disables every breakpoint() call without editing the source — useful for leaving them in during development and skipping them in CI.

Post-mortem debugging

pdb.pm() inspects the stack of the last uncaught exception, after the fact, from the interactive interpreter — no need to have set a breakpoint in advance:

$ python3 -i crashy.py
Traceback (most recent call last):
  File "crashy.py", line 2, in <module>
    1 / 0
ZeroDivisionError: division by zero
>>> import pdb; pdb.pm()
> crashy.py(2)<module>()
-> 1 / 0
(Pdb) l

Running a whole script under the debugger from the start, so it stops automatically at the first unhandled exception, uses python3 -m pdb:

$ python3 -m pdb crashy.py
> crashy.py(1)<module>()
(Pdb) c
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
> crashy.py(2)<module>()
-> 1 / 0
(Pdb)

Performance Basics

Micro-benchmarks with timeit

timeit runs a snippet many times and reports the best timing, avoiding the noise of a single time.time() measurement:

python3 -m timeit -s "data = list(range(1000))" "sorted(data)"
$ python3 -m timeit -s "data = list(range(1000))" "sorted(data)"
20000 loops, best of 5: 12.3 usec per loop

The -s option runs a one-time setup statement (building data) that is excluded from the timing; the timed statement (sorted(data)) is what gets repeated.

Profiling a script with cProfile

cProfile instruments every function call in a run and reports where the time actually went:

python3 -m cProfile -s cumulative slow_script.py
$ python3 -m cProfile -s cumulative slow_script.py
         2814 function calls in 0.842 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.842    0.842 slow_script.py:1(<module>)
        1    0.003    0.003    0.841    0.841 slow_script.py:4(main)
     1000    0.812    0.001    0.812    0.001 slow_script.py:9(fib)

-s cumulative sorts by cumulative time (a function plus everything it calls), which is usually the fastest way to spot the actual bottleneck before reaching for timeit on the specific line found.

Linters and Formatters

The ecosystem has no single mandated tool — any of the following is a reasonable choice, and many projects combine a formatter with a linter:

  • ruff — a fast linter (and formatter) that replaces most of what flake8 plus several plugins used to do:

    pip install ruff
    ruff check .
  • black — an opinionated formatter with almost no configuration; it rewrites code to one consistent style instead of just flagging violations:

    pip install black
    black .
  • flake8 — the longer-standing linter combining pyflakes (error detection) and pycodestyle (style checks), documented at flake8.pycqa.org:

    pip install flake8
    flake8 .

Editor Tooling Recap

IDLE, VS Code, and PyCharm were already introduced for writing and running a first script in Getting Started with Python; all three also drive the debugger described here directly from the editor — clicking a line number to set a breakpoint instead of typing breakpoint(), and stepping with buttons that map to pdb’s `n/s/c commands underneath.

See Also