skip to content

Stopping and Stepping

Halting a program so you can look around: driving the debugger from a paused frame, dropping in after the failure, and attaching to a process nobody started under a debugger.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

12

What does Python's built-in breakpoint() do, and how does PYTHONBREAKPOINT control it?

level: juniorimportance: must knowfreq 45%

answer

  1. A builtin that pauses the program
  2. The pause routes through a hook
  3. sys.breakpointhook chooses the debugger
  4. One environment variable disables every call
  5. Defaults to pdb.set_trace since 3.7

basics

~20 s

Calling breakpoint() pauses the program and drops into a debugger. It dispatches through sys.breakpointhook, which by default calls pdb.set_trace. Setting PYTHONBREAKPOINT=0 makes every call a no-op; setting it to a dotted callable path routes calls there instead.

solid answer

~40 s

`breakpoint()` is a builtin added in Python 3.7 (PEP 553). It does not hard-code a debugger: it forwards all its arguments to whatever `sys.breakpointhook` currently points at, and the default hook calls `pdb.set_trace()`, so execution stops on the next line with a `(Pdb)` prompt. The indirection is the point. The default hook consults the `PYTHONBREAKPOINT` environment variable when it runs, so `PYTHONBREAKPOINT=0` turns every `breakpoint()` call in the process into a no-op without editing code, and `PYTHONBREAKPOINT=some.module.func` sends every call to that callable instead — which is how third-party debuggers hook themselves in. You can also assign `sys.breakpointhook` yourself at runtime and restore it from `sys.__breakpointhook__`. Before 3.7 the equivalent was the two-statement `import pdb; pdb.set_trace()`.

code

python · 8 lines
python
import sys

def log_hook(*args, **kwargs):
    print("paused at breakpoint", args, kwargs)

sys.breakpointhook = log_hook
breakpoint("route-leg", row=6789)
sys.breakpointhook = sys.__breakpointhook__

go deeper

for a junior

Recall that breakpoint() is a builtin needing no import, that it stops the program at an interactive debugger prompt, and that c continues while q quits. Knowing PYTHONBREAKPOINT=0 disables it is the bonus that shows you have used it outside a tutorial.

for a middle

Explain the indirection: the builtin forwards its arguments to sys.breakpointhook, whose default calls pdb.set_trace, and the environment variable is read by that default hook. Be ready to rebind the hook in code and restore it from sys.breakpointhook.

for a senior

Show the operational judgement: disable breakpoints in deployed environments, know that a stray call in a tty-less worker hangs or dies, remember that only the calling thread stops, and enforce a lint rule so debug calls never reach a release branch.

for a principal

Own the policy angle. Decide whether the platform sets PYTHONBREAKPOINT=0 everywhere by default, how developers opt back in locally, whether a shared hook that logs and continues is safer than one that pauses, and how that interacts with the team's debugging and incident practice.

## The builtin and the hook are two different things `breakpoint()` is a plain builtin function, available since **Python 3.7** (PEP 553). Its entire body is: look up `sys.breakpointhook`, call it with whatever positional and keyword arguments `breakpoint()` received, and return the result. It contains no debugger logic at all. The default value of `sys.breakpointhook` calls `pdb.set_trace()`, which is why an unconfigured interpreter stops at a `(Pdb)` prompt. The original, still-valid spelling is `import pdb; pdb.set_trace()` — one import plus one call, on one line, which is exactly the awkwardness PEP 553 removed. The builtin needs no import, is a single short word that is easy to grep for before committing, and — crucially — is *interchangeable*. ## PYTHONBREAKPOINT The default hook reads the `PYTHONBREAKPOINT` environment variable when it is invoked, not once at interpreter start, so changing the process environment at runtime affects later calls. * **Unset** — the debugger starts, as `pdb.set_trace()`. * **`PYTHONBREAKPOINT=0`** — the hook returns immediately. Every `breakpoint()` call in the process becomes a no-op. This is the safety valve: a `breakpoint()` accidentally shipped to a batch job cannot hang it if the environment disables breakpoints globally. * **`PYTHONBREAKPOINT=pkg.mod.func`** — the hook imports `pkg.mod`, looks up `func` and calls it with the forwarded arguments. Any debugger that exposes a suitable entry point can be wired in this way without a source change. If the name cannot be imported, CPython emits a `RuntimeWarning` and the call simply returns rather than crashing your program. ## Setting the hook in code `sys.breakpointhook` is an ordinary module attribute you can rebind. A test harness might point it at a function that records the call and continues; a service might point it at one that logs and raises. `sys.__breakpointhook__` holds the original value so you can restore it, exactly as `sys.__stdout__` mirrors `sys.stdout`. Because the arguments are forwarded, `breakpoint(row, tag="leg")` is meaningful when the hook expects them — a custom hook can accept context. The default `pdb`-based hook accepts a `header` keyword only, so passing arbitrary positional arguments to the *default* hook is an error. ## Where it stops, and what to type After the debugger starts, execution is suspended at the *next* statement to be executed in that frame and you get an interactive prompt: `c` continues, `n` runs the next line, `q` quits the debugger (which raises out of the program via `bdb.BdbQuit`), `p expr` prints an expression evaluated in the paused frame. Everything you type is evaluated in the paused frame's own namespace, so locals are readable and assignable. ## Production hazards worth naming in an interview 1. **It needs a terminal.** `pdb` reads from standard input. In a container, a systemd unit or a worker with no attached tty, a stray `breakpoint()` either hangs the worker forever or dies on a closed stdin. `PYTHONBREAKPOINT=0` in the deployment environment is cheap insurance. 2. **It stops one thread.** `pdb.set_trace()` suspends the thread that executed it. Other threads keep running, keep holding locks and keep taking work, so a paused process is not a frozen snapshot of the whole program. 3. **It is not a logging tool.** A `breakpoint()` left in code is a landmine; a linter rule that fails the build on the builtin is standard practice, and grepping for one short word is why the builtin is easier to police than the two-statement `pdb` form. 4. **It ignores `-O`.** Unlike `assert`, `breakpoint()` is a normal function call and optimisation flags do not remove it — only the environment variable neutralises it. ## The interview shape The question is rarely "what is a breakpoint". It is "why is there a hook in the middle", and the answer is: so that *which* debugger runs, and *whether* one runs at all, is an operational decision made outside the source file — by an environment variable in a deployment, or by an editor that installs its own hook — instead of a code edit that must be reverted before commit.

  • What happens if PYTHONBREAKPOINT names a callable that cannot be imported?
    The default hook fails soft. CPython emits a RuntimeWarning saying the name is unimportable and the breakpoint() call returns immediately, so the program continues rather than crashing. That is deliberate: a typo in a deployment environment variable should not take down a job. You only learn about it from the warning, so treat warnings as visible output in any environment where you set the variable.
  • Why is breakpoint() considered safer to ship accidentally than a bare pdb.set_trace() call?
    Neither is safe, but breakpoint() is centrally neutralisable: PYTHONBREAKPOINT=0 in the deployment environment turns every call in the process into a no-op, while a direct pdb.set_trace() call ignores that variable and will still try to open a prompt. It is also one short, unambiguous token, so a lint rule or a pre-commit grep catches it reliably.
  • Does breakpoint() stop the whole process when other threads are running?
    No. The default hook suspends only the thread that called it. Other threads keep executing, keep acquiring locks and keep consuming work items, so what you inspect at the prompt is one frame stack in a program that is still moving underneath you. For anything concurrent, treat the paused frame as a sample, not a snapshot.

It is a light switch wired to a socket rather than to a bulb: the code says "stop here", and the environment decides which debugger is plugged in — or that nothing is.

saying these in an interview costs you the question

  • Claims breakpoint() is just an alias for pdb.set_trace with no hook
  • Thinks PYTHONBREAKPOINT only picks a debugger and cannot disable breakpoints
  • Believes running with -O strips breakpoint() calls like assert
  • Assumes breakpoint() freezes every thread in the process
  • Says breakpoint() needs an import to work
  • Thinks a bad PYTHONBREAKPOINT value crashes the interpreter

context

open as a page

In pdb, how do the step, next, until and return commands differ when stepping?

level: middleimportance: must knowfreq 50%

basics

~10 s

In pdb, step enters a called function, next runs the call and stays in the current frame, return finishes the current function, and until runs forward past the current line number.

open as a page

How do you drop into pdb on the traceback of an exception you have already caught?

level: middleimportance: must knowfreq 45%

basics

~10 s

Call pdb.post_mortem() with the caught exception's traceback: pdb.post_mortem(exc.__traceback__), or on 3.13+ pass the exception itself. You land in the frame that raised, with its local variables readable but not resumable.

open as a page

What does `python -m pdb script.py` do when the script dies with an uncaught exception?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Instead of printing a traceback and exiting, pdb catches the unhandled exception and drops you into post-mortem debugging at the frame that raised, with that frame's local variables still readable. Execution cannot be resumed from the raise point.

open as a page

Why does Python unbind the name in `except ValueError as exc` when the clause ends?

level: middleimportance: should knowfreq 28%

basics

~20 s

Because the exception references its traceback, which references the frames, which reference the exception - a cycle that would keep the whole stack alive. Python deletes the name at the end of the clause; assign the exception elsewhere first if you need it later.

open as a page

What does sys.remote_exec(pid, script) do in Python 3.14, and in which process does the script run?

level: middleimportance: should knowfreq 22%

basics

~20 s

sys.remote_exec, added by PEP 768 in Python 3.14, asks an already-running interpreter, named by its process id, to execute a Python file. That file runs inside the target process's main thread, never in the caller.

open as a page

How do you use pdb's conditional breakpoints to catch a duplicated side effect in a 6,800-row batch?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Break on the code that performs the side effect, not on the loop, with a condition so it fires only for the offending row. Then use where and up to compare the caller frames.

open as a page

How do you capture post-mortem state from a headless nightly ad-auction bidder that crashes once a week?

level: seniorimportance: should knowfreq 30%

basics

~20 s

There is no terminal to hold a debugger prompt, so capture instead of attach: at the top-level handler, serialize the exception's frames with their locals - traceback.TracebackException with capture_locals=True - and write that artefact where you can read it later.

open as a page

Why might a script sent with sys.remote_exec never run in a process that has stopped making progress?

level: seniorimportance: should knowfreq 18%

basics

~20 s

Python 3.14 runs an injected script only at a safe point, between complete bytecode instructions on the target's main thread. A main thread parked in a blocking system call or a long C call never reaches one, so the queued request simply waits.

open as a page

Would you leave Python 3.14's remote-attach debugging enabled in production images, and how do you decide?

level: principalimportance: should knowfreq 14%

basics

~20 s

Usually yes, because the real gate is the operating-system permission needed to write another process's memory, not the interpreter switch. Disable it where policy forbids in-process code injection, or where nobody could ever be authorised to attach.

open as a page

In pdb, how do you print a variable whose name collides with a command such as n or c?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

The pdb prompt matches its own commands first, so typing n runs next. Use p n to print the value, pp n to pretty-print it, or an exclamation-mark prefix to run the line as Python.

open as a page

How do you disable Python 3.14's remote-attach debugging for a hardened deployment?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

Set PYTHON_DISABLE_REMOTE_DEBUG in the environment of the process being attached to, pass -X disable-remote-debug on its command line, or build the interpreter with the --without-remote-debug configure option. All three apply to the target, at its startup.

open as a page