Hyppää sisältöön

Ei vielä käännetty

Tätä sivua ei ole vielä käännetty suomeksi, joten se näytetään englanniksi. Auta kääntämään

bdb Module Complexity

The bdb module is the machinery under pdb. Bdb installs a trace function, receives the events it asks for, and decides at each one whether to stop and hand control to a subclass's user_* method. A debugged program pays per event, so which events arrive, not the size of the program, is what the cost turns on.

Breakpoints live in two class-level registries on Breakpoint, shared by every Bdb in the process. Each Bdb also keeps, per file, a list of the lines holding a breakpoint.

e is the trace events delivered while the debugged code runs, d is frames on the stack, p is skip patterns, b is lines holding a breakpoint in the file being checked, k is breakpoints at one line, L is file:line locations holding a breakpoint across all files, B is breakpoint numbers ever assigned, deleted ones included, and c is files in the linecache cache. File names, module names, source lines and breakpoint conditions are priced at O(1); evaluating a condition costs whatever its expression does, and a subclass's user_* methods cost whatever they do. One-time cache fills are left out of the bounds: linecache reading a whole file the first time a line of it is needed, canonic() resolving a new file name, and on 3.14+ break_anywhere() recording a code object's line numbers the first time it sees it. Bounds are for the default 'settrace' backend.

Complexity Reference

Bdb

Operation Time Space Notes
bdb.Bdb(skip=None, backend='settrace') O(p + L·b) O(p + L) Copies every breakpoint location already registered, by any instance, checking each against its file's lines so far; backend is 3.14+
Bdb.run(cmd, globals=None, locals=None), Bdb.runeval(expr, globals=None, locals=None), Bdb.runctx(cmd, globals, locals) O(c + e·(p + b + k)) O(c) Plus the code's own cost; a string is compiled first, and BdbQuit ends the run quietly
Bdb.runcall(func, /, *args, **kwds) O(c + e·(p + b + k)) O(c) Plus the call's own cost; returns what func returns
Bdb.reset() O(c) O(c) Revalidates every file in linecache; the run* methods and set_trace() call it
Bdb.set_trace(frame=None) O(c + d) O(c + d) Hooks every frame on the stack; 3.13+ stops at once and keeps each frame's trace flags until a set_continue() with no breakpoints set
Bdb.start_trace(), Bdb.stop_trace() O(1) O(1) 3.14+; install or remove the trace function
Bdb.canonic(filename) O(1) O(1) Cached per name in fncache; a name in angle brackets such as <string> comes back unchanged

Bdb event dispatch

Operation Time Space Notes
Bdb.trace_dispatch(frame, event, arg) O(p + b + k) O(1) One event, handed to the matching dispatch_* method
Bdb.dispatch_line(frame) O(p + b + k) O(1) The k term and the conditions evaluated come from effective(), on a line holding breakpoints
Bdb.dispatch_call(frame, arg) O(p + b) O(1) Returns None for a frame that cannot stop, so its lines raise no events
Bdb.dispatch_return(frame, arg), Bdb.dispatch_exception(frame, arg) O(p) O(1)
Bdb.stop_here(frame), Bdb.is_skipped_module(module_name) O(p) O(1) One fnmatch per skip pattern, up to the first that matches
Bdb.break_here(frame) O(b + k) O(1) Checks the line, then the function's first line for a breakpoint set by function name
Bdb.break_anywhere(frame) O(b) O(1) 3.14+ looks for the file's breakpoint lines in the frame's code, caching each code object's lines on first use; earlier it is O(1), and any breakpoint in the file counts
Bdb.user_call(frame, argument_list), Bdb.user_line(frame), Bdb.user_return(frame, return_value), Bdb.user_exception(frame, exc_info) O(1) O(1) Do nothing; a subclass overrides them, and their cost is added at every stop
Bdb.do_clear(arg) O(1) O(1) Raises NotImplementedError; a subclass supplies it to delete a temporary breakpoint once hit
Bdb.disable_current_event(), Bdb.restart_events() O(1) O(1) 3.14+; do nothing under the 'settrace' backend

Bdb stepping

Operation Time Space Notes
Bdb.set_step(), Bdb.set_next(frame), Bdb.set_return(frame), Bdb.set_until(frame, lineno=None) O(1) O(1) Record where to stop next; on 3.13+ the first of these or set_continue() after set_trace() walks the stack, O(d), to end instruction stepping
Bdb.set_continue() O(1) O(1) With no breakpoints set it removes tracing, O(d), and the code runs on untraced
Bdb.set_quit() O(1) O(1) The dispatch that called it raises BdbQuit

Bdb breakpoints

Operation Time Space Notes
Bdb.set_break(filename, lineno, temporary=False, cond=None, funcname=None) O(b + d·b) O(1) Returns an error string for a line that does not exist; during a stop, 3.12.9+ and 3.13.1+ also run break_anywhere() on every frame of the stack
Bdb.clear_break(filename, lineno) O(b + k²) O(k) Each deletion shifts the rest of that line's list
Bdb.clear_bpbynumber(arg) O(b + k) O(1)
Bdb.clear_all_file_breaks(filename) O(b·k²) O(1) At a line holding several breakpoints, every second one survives in Breakpoint.bplist
Bdb.clear_all_breaks() O(B·k) O(1) Walks every number ever assigned, deleted ones included
Bdb.get_bpbynumber(arg) O(1) O(1) Raises ValueError for a non-numeric, unknown or deleted number
Bdb.get_break(filename, lineno) O(b) O(1)
Bdb.get_breaks(filename, lineno) O(b) O(1) The registry's own list, not a copy
Bdb.get_file_breaks(filename) O(1) O(1) The instance's own list of lines, not a copy
Bdb.get_all_breaks() O(1) O(1) The instance's own dict, not a copy

Bdb stack inspection

Operation Time Space Notes
Bdb.get_stack(f, t) O(d + t) O(d + t) t = traceback entries; returns (frame, lineno) pairs and the index of f
Bdb.format_stack_entry(frame_lineno, lprefix=': ') O(1) O(1) Plus reprlib.repr() of a return value: it shortens the text, but sorts a dict's or set's keys first

Breakpoint

Operation Time Space Notes
bdb.Breakpoint(file, line, temporary=False, cond=None, funcname=None) O(1) O(1) Takes the next number and joins the class-level bpbynumber and bplist
Breakpoint.deleteMe() O(k) O(1) Leaves None at its number: numbers are never reused
Breakpoint.enable(), Breakpoint.disable() O(1) O(1) A disabled breakpoint is passed over without counting a hit
Breakpoint.bpformat(), Breakpoint.bpprint(out=None) O(1) O(1) bpprint() writes bpformat() to out, sys.stdout by default
Breakpoint.file, Breakpoint.line, Breakpoint.temporary, Breakpoint.cond, Breakpoint.funcname, Breakpoint.enabled, Breakpoint.ignore, Breakpoint.hits O(1) O(1) Plain attributes; effective() updates hits and ignore
Breakpoint.bpbynumber, Breakpoint.bplist O(1) O(1) The shared registries: a list indexed by number and a dict of lists keyed by (file, line)

Module functions and exceptions

Operation Time Space Notes
bdb.effective(file, line, frame) O(k) O(1) Plus each condition it evaluates; returns the first enabled breakpoint whose condition holds and whose ignore count is spent, or whose condition raises
bdb.checkfuncname(b, frame) O(1) O(1)
bdb.set_trace() O(c + d + L·b) O(c + d + L) Builds a Bdb and calls its set_trace()
bdb.BdbQuit O(1) O(1) Raised to end a run; the run* methods catch it

Writing a Debugger

A subclass overrides the user_* methods and, at each stop, calls one of the set_* methods to say where to stop next. A run starts by stopping at the first line, so a set_step() at every stop visits every line event.

import bdb

class Recorder(bdb.Bdb):
    def __init__(self):
        super().__init__()
        self.stops = []

    def user_line(self, frame):  # called at every stop
        self.stops.append(frame.f_lineno - work.__code__.co_firstlineno)
        self.set_step()  # O(1) - stop again at the next line

def work():
    total = 0
    for i in range(3):
        total += i
    return total

debugger = Recorder()
assert debugger.runcall(work) == 3  # O(c + e·(p + b + k)), plus work's own cost
assert set(debugger.stops) == {1, 2, 3, 4}  # every line of work, some of them repeatedly

What Gets Traced

With the 'settrace' backend, every call raises a call event, and dispatch_call() returns None for a frame that cannot stop, so that frame raises no line events. A frame already being traced, such as the one a stop happened in, raises one per line executed, at O(p + b) each away from breakpoints. set_continue() with no breakpoints set removes tracing altogether.

import bdb

class Counter(bdb.Bdb):
    def __init__(self):
        super().__init__()
        self.line_events = 0

    def dispatch_line(self, frame):
        self.line_events += 1
        return super().dispatch_line(frame)

    def user_line(self, frame):
        self.set_continue()  # O(1) - run on to the next breakpoint

def loop(n):
    total = 0
    for i in range(n):
        total += i
    return total

def caller(n):
    return loop(n)

def line_events(func, n, with_breakpoint):
    bdb.Breakpoint.clearBreakpoints()  # the registry is shared by every Bdb
    debugger = Counter()
    if with_breakpoint:
        # A breakpoint in another file, in a function that never runs
        line = bdb.effective.__code__.co_firstlineno + 1
        assert debugger.set_break(bdb.__file__, line) is None
    debugger.runcall(func, n)
    return debugger.line_events

# No breakpoints: continuing removes tracing, so only the first stop counts
assert line_events(loop, 10_000, with_breakpoint=False) == 1

# A breakpoint elsewhere: loop() cannot stop, so its lines raise no events
assert line_events(caller, 10_000, with_breakpoint=True) == 1

# The frame the stop happened in keeps raising them, once per line: O(n)
assert line_events(loop, 10_000, with_breakpoint=True) > 10_000

On 3.13 and earlier, dispatch_call() treats any breakpoint in a frame's file as a reason to trace it, so while running on to a breakpoint every function called in that file raises line events. From 3.14 only a function whose code holds the breakpoint does, and Bdb(backend='monitoring') goes further: it can switch off a line event that cannot stop, rather than receive it on every execution.

Skipping Modules

skip takes glob patterns for module names. Stepping never stops in a frame from a matching module, but an event can pay O(p) to match the frame's module against the patterns.

import bdb
import json

class Stepper(bdb.Bdb):
    def __init__(self, skip=None):
        super().__init__(skip=skip)  # O(p) to store the patterns
        self.modules = set()

    def user_line(self, frame):
        self.modules.add(frame.f_globals.get('__name__'))
        self.set_step()

def work():
    return json.dumps([1, 2])

stepper = Stepper()
stepper.runcall(work)
assert 'json.encoder' in stepper.modules

skipping = Stepper(skip=['json', 'json.*'])
skipping.runcall(work)
assert skipping.modules == {'__main__'}

Managing Breakpoints

Each Bdb keeps its breakpoint lines in one list per file, so finding a line is O(b). The Breakpoint objects themselves live in the shared registries: a Bdb built later sees every location registered so far, and a deleted breakpoint leaves None at its number for good.

import bdb

bdb.Breakpoint.clearBreakpoints()  # start from an empty registry
first = bdb.Bdb()
filename = first.canonic(bdb.__file__)  # O(1) once cached
line = bdb.effective.__code__.co_firstlineno + 1

assert first.set_break(filename, line) is None  # O(b) outside a stop
assert first.set_break(filename, line, cond='False') is None
assert len(first.get_breaks(filename, line)) == 2  # O(b)
assert first.get_file_breaks(filename) == [line]  # O(1)
assert first.set_break(filename, 10**6) == f'Line {filename}:1000000 does not exist'

# A Bdb built later copies every registered location - O(L·b)
second = bdb.Bdb()
assert second.get_break(filename, line)  # O(b)

# Deleting by number leaves None behind: numbers are never reused
assert first.clear_bpbynumber('1') is None  # O(b + k)
assert bdb.Breakpoint.bpbynumber[1] is None
try:
    first.get_bpbynumber('1')  # O(1)
except ValueError as error:
    assert 'already deleted' in str(error)
else:
    raise AssertionError('a deleted breakpoint was found')

assert first.clear_all_breaks() is None  # O(B·k), deleted numbers included
assert first.get_all_breaks() == {}

Choosing the Active Breakpoint

effective() walks the k breakpoints at a line in the order they were set. It passes over a disabled one without counting a hit, and one set on a function name unless this is the function's first line; every other one it reaches counts a hit. The first whose condition holds and whose ignore count is spent is the one that stops, and so is one whose condition raises.

import bdb
import sys

def here():
    return sys._getframe()

frame = here()
file, line = frame.f_code.co_filename, frame.f_lineno

bdb.Breakpoint.clearBreakpoints()
off = bdb.Breakpoint(file, line)  # O(1)
off.disable()
never = bdb.Breakpoint(file, line, cond='False')
second_hit = bdb.Breakpoint(file, line)
second_hit.ignore = 1

assert bdb.effective(file, line, frame) == (None, None)  # O(k), ignore count spent
assert bdb.effective(file, line, frame) == (second_hit, True)
assert (off.hits, never.hits, second_hit.hits) == (0, 2, 2)

Clearing a File's Breakpoints

clear_all_file_breaks() deletes breakpoints from a line's list while iterating over it, so at a line holding several, every second one survives. The instance forgets the file anyway, but the survivors stay in Breakpoint.bplist, and a Bdb built later picks them up again. clear_break() iterates over a copy and removes them all.

import bdb

bdb.Breakpoint.clearBreakpoints()
debugger = bdb.Bdb()
filename = debugger.canonic(bdb.__file__)
line = bdb.effective.__code__.co_firstlineno + 1
for _ in range(4):
    debugger.set_break(filename, line)

assert debugger.clear_all_file_breaks(filename) is None  # O(b·k²)
assert debugger.get_file_breaks(filename) == []
assert len(bdb.Breakpoint.bplist[filename, line]) == 2

later = bdb.Bdb()  # O(L·b) - sees the survivors
assert later.clear_break(filename, line) is None  # O(b + k²)
assert (filename, line) not in bdb.Breakpoint.bplist

Inspecting the Stack

get_stack() walks the stack and then the traceback, collecting (frame, lineno) pairs. format_stack_entry() formats one of them; a return value is cut short by reprlib, so a long list costs no more than a short one.

import bdb
import sys

def depth(n):
    return depth(n - 1) if n else sys._getframe()

def returns_big():
    __return__ = list(range(100_000))
    return sys._getframe()

debugger = bdb.Bdb()
debugger.reset()  # O(c)

frame = depth(50)
stack, index = debugger.get_stack(frame, None)  # O(d + t)
assert stack[-1][0] is frame and index == len(stack) - 1
assert len(stack) > 50

frame = returns_big()
entry = debugger.format_stack_entry((frame, frame.f_lineno))  # O(1)
assert '->[0, 1, 2, 3, 4, 5, ...]' in entry

Common Patterns

Logging a Conditional Breakpoint

A debugger that records a value each time a conditional breakpoint fires and runs on. Only the frame the run first stopped in is traced, so each of its lines costs O(p + b), and the breakpoint line adds O(k) and its condition.

import bdb

class HitLogger(bdb.Bdb):
    def __init__(self):
        super().__init__()
        self.seen = []

    def user_line(self, frame):
        if self.get_break(frame.f_code.co_filename, frame.f_lineno):  # O(b)
            self.seen.append(frame.f_locals['i'])
        self.set_continue()  # O(1) while a breakpoint is set

def work(n):
    total = 0
    for i in range(n):
        total += i
    return total

bdb.Breakpoint.clearBreakpoints()
logger = HitLogger()
line = work.__code__.co_firstlineno + 3
assert logger.set_break(__file__, line, cond='i % 25 == 0') is None
assert logger.runcall(work, 100) == 4950
assert logger.seen == [0, 25, 50, 75]

Performance Best Practices

✅ Do:

  • Call set_continue() when nothing is left to stop at: with no breakpoints it removes tracing and the code runs at full speed
  • Put breakpoints in the functions you are debugging; on 3.13 and earlier, running on to one traces every function called in its file line by line
  • Use Bdb(backend='monitoring') on 3.14+ for long runs between stops
  • Clear breakpoints with clear_break() or clear_all_breaks(), which remove every one at a line

❌ Avoid:

  • Stepping with set_step() through code you do not need to see; every line event outside a skipped module reaches user_line()
  • Long skip lists: an event matches the frame's module against every pattern until one fits
  • clear_all_file_breaks() on a line holding several breakpoints; every second one survives

Version Notes

  • Python 3.14+: Bdb(backend='monitoring') runs on sys.monitoring; start_trace(), stop_trace(), disable_current_event() and restart_events() added
  • Python 3.14+: break_anywhere() looks for a breakpoint in the frame's own code, so while running on to a breakpoint only a called function holding one raises line events; earlier, every function in its file did
  • Python 3.13+: set_trace() stops at once, on the next instruction, rather than on the next line
  • Python 3.12.9+ and 3.13.1+: set_break() during a stop re-checks every frame on the stack
  • pdb - the interactive debugger built on Bdb
  • sys - sys.settrace() and sys.monitoring, the two tracing backends
  • linecache - the source-line cache that reset() revalidates