# How to Debug a Python Script with pdb


When a script prints the wrong number or crashes with a traceback, `print()` calls only go so far. [pdb](https://pydevtools.com/handbook/reference/pdb.md), Python's built-in debugger, pauses the program mid-run so you can inspect variables and step through the code line by line until you reach the frame that failed. It ships in the standard library, so there is nothing to install.

Every command below runs the same on macOS, Linux, and Windows. This guide uses one small script with a bug:

```python {filename="stats.py"}
def average(readings):
    return sum(readings) / len(readings)


def summarize(batches):
    for name, readings in batches.items():
        print(f"{name}: avg {average(readings):.1f}")


summarize({"morning": [21, 22, 20], "evening": []})
```

Running it crashes on the empty `"evening"` list:

```console
$ python stats.py
morning: avg 21.0
ZeroDivisionError: division by zero
```

> [!TIP]
> On a [uv](https://pydevtools.com/handbook/reference/uv.md)-managed project, prefix each command with `uv run` (`uv run python stats.py`, `uv run python -m pdb stats.py`) so it uses the project's virtual environment. `breakpoint()` and every debugger command behave the same.

## Pause at a line with breakpoint()

Add a `breakpoint()` call on the line you want to stop at, then run the script the normal way:

```python
def average(readings):
    breakpoint()
    return sum(readings) / len(readings)
```

```console
$ python stats.py
> stats.py(2)average()
-> breakpoint()
(Pdb)
```

The program is paused on the `breakpoint()` line, and the `(Pdb)` prompt accepts debugger commands. The [built-in `breakpoint()`](https://docs.python.org/3/library/functions.html#breakpoint) function arrived in Python 3.7; on older versions, write `import pdb; pdb.set_trace()` instead.

## Inspect state at the prompt

`p` evaluates an expression and prints it, `pp` pretty-prints, and `args` shows the current function's arguments:

```text
(Pdb) p readings
[21, 22, 20]
(Pdb) args
readings = [21, 22, 20]
(Pdb) p len(readings)
3
```

`l` (list) shows the source around the current line, with `->` marking where execution is paused:

```text
(Pdb) l
  1  	def average(readings):
  2  ->	    breakpoint()
  3  	    return sum(readings) / len(readings)
```

`whatis expr` prints an expression's type, and `interact` opens a full Python REPL against the current frame's variables. The [pdb Reference](https://pydevtools.com/handbook/reference/pdb.md) has the complete command table.

## Move through the code

Five commands cover most stepping:

- `n` (next): run the current line, stepping over any function it calls
- `s` (step): run the current line, stepping into a function call
- `c` (continue): resume until the next breakpoint or the end of the program
- `r` (return): run until the current function returns
- `unt` (until): run until a line past the current one is reached

Continuing past the first hit reveals the bug. The `breakpoint()` fires again for the `"evening"` batch, and its readings are empty:

```text
(Pdb) c
morning: avg 21.0
> stats.py(2)average()
-> breakpoint()
(Pdb) p readings
[]
(Pdb) p len(readings)
0
```

`len(readings)` is `0`, so the next line divides by zero.

## Exit pdb

`q` (quit) stops the debugger and aborts the program. `c` (continue) resumes the program; with no further breakpoint to hit, it runs to completion. Pressing Ctrl-D at the `(Pdb)` prompt sends end-of-file and exits the same way as `q`.

## Start pdb without editing the file

To debug code you cannot or would rather not modify, such as a third-party module, run the script under pdb instead of inserting `breakpoint()`:

```console
$ python -m pdb stats.py
> stats.py(1)<module>()
-> def average(readings):
(Pdb)
```

The debugger starts paused before the first line. Set a breakpoint on a function or line number, then continue to it:

```text
(Pdb) b average
Breakpoint 1 at stats.py:2
(Pdb) c
> stats.py(2)average()
-> return sum(readings) / len(readings)
```

To run a module the way `python -m` would, use `python -m pdb -m mypackage.cli`.

## Manage breakpoints from the prompt

pdb numbers each breakpoint as you set it, and those numbers drive the rest of the commands:

```text
(Pdb) b average
Breakpoint 1 at stats.py:2
(Pdb) disable 1
Disabled breakpoint 1 at stats.py:2
(Pdb) enable 1
Enabled breakpoint 1 at stats.py:2
(Pdb) cl 1
Deleted breakpoint 1 at stats.py:2
```

`disable N` switches a breakpoint off without losing it, `enable N` turns it back on, and `cl N` (clear) deletes it. To pause only when a condition holds, attach it with `condition`:

```text
(Pdb) b average
Breakpoint 1 at stats.py:2
(Pdb) condition 1 len(readings) == 0
New condition set for breakpoint 1.
(Pdb) c
morning: avg 21.0
> stats.py(2)average()
-> return sum(readings) / len(readings)
(Pdb) p readings
[]
```

The debugger skips the healthy `"morning"` batch and stops only on the empty one. `tbreak` sets a breakpoint that clears itself after the first hit, and `ignore N count` skips the next `count` hits.

## Debug a crash after it happens

To inspect a crash without setting any breakpoints, run the script under pdb with `-c continue`. It runs normally and drops into a post-mortem session on the frame that raised:

```console
$ python -m pdb -c continue stats.py
morning: avg 21.0
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> stats.py(2)average()
-> return sum(readings) / len(readings)
(Pdb) p readings
[]
```

The prompt opens in `average()` with `readings` bound to the empty list that caused the `ZeroDivisionError`. From an interactive session, call `pdb.pm()` after a crash to reach the same frame:

```text
>>> import pdb; pdb.pm()
> stats.py(2)average()
-> return sum(readings) / len(readings)
(Pdb) p readings
[]
```

## Turn breakpoints off in production

Set `PYTHONBREAKPOINT=0` to make every `breakpoint()` call a no-op, so a stray call can never pause a deployed service:

```console
$ PYTHONBREAKPOINT=0 python stats.py
```
```powershell
$env:PYTHONBREAKPOINT=0; python stats.py
```
Pointing the variable at a dotted path routes `breakpoint()` to a different debugger. Setting `PYTHONBREAKPOINT=IPython.terminal.debugger.set_trace` opens [IPython](https://pydevtools.com/handbook/reference/ipython.md)'s debugger instead of pdb.

## Learn More

- [pdb Reference](https://pydevtools.com/handbook/reference/pdb.md) covers the full command table and the `.pdbrc` startup file for persistent aliases.
- [The official pdb documentation](https://docs.python.org/3/library/pdb.html) documents every command and flag.
- [`PYTHONBREAKPOINT` environment variable](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONBREAKPOINT) documents the disable and reroute behavior.
