Python sys.breakpointhook() Explained

When you call breakpoint() in Python, the actual work of dropping into a debugger session is handled behind the scenes by sys.breakpointhook() — a hook function you can inspect, replace, and use to point Python at any debugger you want.

Before Python 3.7, dropping into the debugger meant writing import pdb; pdb.set_trace() every time. PEP 553 fixed that by introducing the breakpoint() built-in and its underlying engine, sys.breakpointhook(). Understanding the hook gives you full control over what happens at every breakpoint in your code — whether you want to disable debugging entirely in production, swap in a third-party debugger, or write your own custom debugging logic.

What Is sys.breakpointhook()?

sys.breakpointhook() is the function the built-in breakpoint() calls internally. By default, it drops you into pdb, Python's standard command-line debugger. The entire purpose of the separation is flexibility: because breakpoint() delegates to a hook, you can change the hook without touching any call site in your code.

The function signature accepts arbitrary positional and keyword arguments:

sys.breakpointhook(*args, **kws)

Whatever arguments you pass to breakpoint() are forwarded straight through to sys.breakpointhook(), which then forwards them to the underlying debugger. The return value travels the same path in reverse: whatever sys.breakpointhook() returns is what breakpoint() returns to the call site.

Version Note

Both breakpoint() and sys.breakpointhook() were added in Python 3.7. If you need to support older versions, you cannot rely on either of them.

How the Hook Works Under the Hood

The default implementation of sys.breakpointhook() follows a specific decision tree every time it is invoked. Understanding that sequence helps you predict exactly what will happen at any given breakpoint.

First, the hook reads the environment variable PYTHONBREAKPOINT. Based on what it finds, one of three things happens:

  1. If PYTHONBREAKPOINT is set to "0", the function returns immediately without entering any debugger. This is the canonical way to disable all breakpoints at runtime.
  2. If PYTHONBREAKPOINT is unset or set to the empty string, pdb.set_trace() is called. This is the standard default behavior.
  3. If PYTHONBREAKPOINT contains a dotted import path (for example pudb.set_trace), the hook imports the named module and calls the named callable, passing along *args and **kws.

Here is a simplified version of what the default hook looks like internally, drawn from PEP 553:

import importlib, os, warnings

def breakpointhook(*args, **kws):
    hookname = os.getenv('PYTHONBREAKPOINT')

    # Disabled
    if hookname == '0':
        return None

    # Default: use pdb
    if not hookname:
        hookname = 'pdb.set_trace'

    modname, _, funcname = hookname.rpartition('.')
    if not modname:
        modname = 'builtins'

    try:
        module = importlib.import_module(modname)
        hook = getattr(module, funcname)
    except Exception:
        warnings.warn(
            f'Ignoring unimportable $PYTHONBREAKPOINT: {hookname}',
            RuntimeWarning
        )
        return None

    return hook(*args, **kws)
Important

If importing the callable named in PYTHONBREAKPOINT fails, Python issues a RuntimeWarning and silently skips the breakpoint rather than crashing your program. Always test your custom hook before relying on it.

Controlling It with PYTHONBREAKPOINT

The PYTHONBREAKPOINT environment variable is the least invasive way to change debugger behavior because it requires no code changes. You set it before running your script and the hook picks it up automatically.

Disabling all breakpoints

# Run the script with all breakpoints disabled
PYTHONBREAKPOINT=0 python my_script.py

This is useful in staging or production environments where breakpoint calls may have been accidentally committed. Setting the variable to "0" turns every breakpoint() call into a no-op with no runtime overhead beyond the initial environment variable check.

Switching to a third-party debugger

# Use pudb (a full-screen terminal debugger)
PYTHONBREAKPOINT=pudb.set_trace python my_script.py

# Use web-pdb (a browser-based debugger)
PYTHONBREAKPOINT=web_pdb.set_trace python my_script.py

The value must be a Python dotted-import path. The hook will split on the final dot, import everything to the left as a module, and look up everything to the right as an attribute on that module. If the callable is in builtins, you can use a name with no dots at all (for example, PYTHONBREAKPOINT=int would call int(), which is occasionally useful for testing the mechanism itself).

Pro Tip

Set PYTHONBREAKPOINT=0 as a default in your production deployment environment variables. That way, any breakpoint that slips through code review becomes harmless rather than halting a live process.

Replacing the Hook Programmatically

You can also replace sys.breakpointhook directly in your code by assigning a new callable to it. This gives you more flexibility than the environment variable because you can write arbitrary Python logic inside the hook.

One common use case is logging every breakpoint hit to a file during automated test runs, rather than pausing execution:

import sys

def logging_hook(*args, **kws):
    import traceback
    with open('breakpoints.log', 'a') as f:
        f.write('--- Breakpoint hit ---\n')
        traceback.print_stack(file=f)
    # Don't actually pause; just record and continue

sys.breakpointhook = logging_hook

# Anywhere after this assignment, breakpoint() calls logging_hook instead of pdb
def process_data(data):
    result = transform(data)
    breakpoint()   # logs a stack trace, does not pause
    return result

Another common pattern is delegating to different debuggers depending on the environment:

import sys
import os

def smart_hook(*args, **kws):
    if os.getenv('CI'):
        # In a CI pipeline, just log and continue
        import traceback
        traceback.print_stack()
        return
    try:
        import pudb
        pudb.set_trace()
    except ImportError:
        import pdb
        pdb.set_trace()

sys.breakpointhook = smart_hook
Note

Once you replace sys.breakpointhook programmatically, the PYTHONBREAKPOINT environment variable is no longer consulted. Your custom function takes full responsibility for all behavior at that point.

sys.__breakpointhook__ and Restoring the Default

Python stores the original, unmodified hook at sys.__breakpointhook__ (note the double underscores). This is a read-only reference to the factory default, preserved for exactly one purpose: letting you restore the original behavior after you have replaced it.

import sys

# Replace the hook
sys.breakpointhook = my_custom_hook

# ... do your work ...

# Restore the original pdb-based hook
sys.breakpointhook = sys.__breakpointhook__

This pattern mirrors how sys.__displayhook__ and sys.__excepthook__ work. All three double-underscore variants serve as restore points. sys.__breakpointhook__ was added in Python 3.7 alongside the hook itself.

IDEs and testing frameworks use this pattern heavily. pytest, for example, replaces sys.breakpointhook with its own implementation when it starts, then restores sys.__breakpointhook__ during teardown so that subsequent test runners are not affected.

Pro Tip

If you are writing a library or framework that temporarily replaces sys.breakpointhook, always restore sys.__breakpointhook__ in a finally block or a teardown method. Leaving a modified hook in place can silently break debugging for anything running in the same process afterward.

Key Takeaways

  1. sys.breakpointhook() is the engine behind breakpoint(): Every call to the built-in breakpoint() delegates directly to this hook. Controlling the hook means controlling all breakpoints in your program from a single point.
  2. PYTHONBREAKPOINT gives you zero-code-change control: Setting it to "0" disables all breakpoints; setting it to a dotted import path switches debuggers. Neither approach requires modifying any source file.
  3. Programmatic replacement is more powerful but disables the environment variable: Assigning a new callable to sys.breakpointhook lets you add custom logic, but from that point forward PYTHONBREAKPOINT is ignored. Use this only when you need logic the environment variable cannot express.
  4. sys.__breakpointhook__ is your safety net: The original hook is always available here. Restore it when your custom hook's scope ends, especially inside libraries and test frameworks.
  5. Third-party debuggers integrate through this hook: Tools like pudb, web-pdb, and IDE debuggers hook into sys.breakpointhook the same way you would. Understanding the mechanism helps you configure, troubleshoot, and extend those integrations confidently.

sys.breakpointhook() is a small function with an outsized impact on your debugging workflow. Once you know it exists, you will find it at the center of every Python debugging configuration, from environment-level disabling in CI pipelines to per-session debugger switching in development. The hook is the knob; breakpoint() is just the button that turns it.

back to articles