Unveiling Python’s pdb: How the Built‑in Debugger Works Under the Hood
This article dissects Python’s standard pdb module, explaining its pure‑Python implementation on top of bdb and cmd, the event‑driven tracing mechanism, core debugging commands such as step, next, continue, and how MNN Workbench embeds these capabilities for mobile AI development.
Background
With the rapid growth of mobile internet, artificial intelligence is increasingly deployed on mobile devices, and Python remains the preferred language for algorithm development. However, deploying and debugging algorithms on mobile endpoints has traditionally relied on inserting log statements, which becomes inefficient as projects grow.
Embedding pdb in MNN Workbench
The MNN Workbench adds side‑car Python debugging capabilities by integrating the official pdb module. Developers familiar with pdb can use its interactive commands—breakpoints, step execution, and variable inspection—directly on mobile‑side code.
pdb Architecture
In CPython, pdb is not a built‑in C extension; it is a pure‑Python wrapper that inherits from bdb.Bdb and cmd.Cmd. The core class is defined as:
class Pdb(bdb.Bdb, cmd.Cmd):
...The cmd module provides an interactive command loop, while bdb implements the tracing logic using sys.settrace. The tracer dispatches four event types— call, line, return, and exception —to pdb, which decides whether to pause based on the current debugging command.
Basic Flow
pdb starts and binds the current frame to trace_dispatch.
def trace_dispatch(self, frame, event, arg):
if self.quitting:
return # None
if event == 'line':
return self.dispatch_line(frame)
if event == 'call':
return self.dispatch_call(frame, arg)
if event == 'return':
return self.dispatch_return(frame, arg)
if event == 'exception':
...Each dispatched method eventually calls stop_here to determine if execution should be interrupted.
Interrupt Control
Interrupt control decides whether a debugging command should pause execution at a given frame. The central method is:
def stop_here(self, frame):
if self.skip and self.is_skipped_module(frame.f_globals.get('__name__')):
return False
if frame is self.stopframe:
if self.stoplineno == -1:
return False
return frame.f_lineno >= self.stoplineno
while frame is not None and frame is not self.stopframe:
if frame is self.botframe:
return True
frame = frame.f_back
return FalseCommands such as s (step), n (next), c (continue), r (return), and unt (until) manipulate stopframe, stoplineno, and related attributes to achieve the desired granularity.
Key Debugging Commands
s (step) : Executes the next line; if the line is a function call, execution stops at the first line of the called function.
n (next) : Executes until the next line in the current frame, ignoring lines inside called functions.
c (continue) : Runs until the next breakpoint is hit.
r (return) : Runs until the current function returns.
unt (until) : Runs until a specified line is reached; for loops are traced only once.
up / d (down) : Moves the interactive view up or down the call stack.
b (break) : Sets a breakpoint without immediately affecting execution; the breakpoint becomes active when the traced line matches the stored line number.
Stack Frame Information
Each frame contains the execution context of a function call. pdb retrieves the call stack via get_stack, which combines the normal call stack with any exception stack:
def get_stack(self, f, t):
stack = []
if t and t.tb_frame is f:
t = t.tb_next
while f is not None:
stack.append((f, f.f_lineno))
if f is self.botframe:
break
f = f.f_back
stack.reverse()
i = max(0, len(stack) - 1)
while t is not None:
stack.append((t.tb_frame, t.tb_lineno))
t = t.tb_next
return stack, iThe stack is displayed and navigated with do_up and do_down, which adjust the current index and print the selected frame.
Breakpoints
When a breakpoint is set on a function, pdb checks the function’s first executable line ( co_firstlineno) to determine if the current line matches the breakpoint. If it does, execution pauses; otherwise, the breakpoint is ignored until the function’s entry point is reached.
def break_here(self, frame):
lineno = frame.f_lineno
if lineno not in self.breaks[filename]:
lineno = frame.f_code.co_firstlineno
if lineno not in self.breaks[filename]:
return False
(bp, flag) = effective(filename, lineno, frame)
...Practical Example
A simple Python script is used to illustrate the flow of commands. The execution passes through three frames: the root __main__ frame, a function frame, and a nested function frame. The article walks through a sequence of user inputs (e.g., unt, s, c, r, up, down, n) and shows how pdb’s internal logic responds at each step.
Conclusion
The Python standard library’s pdb implementation is relatively straightforward: it builds on bdb for tracing and cmd for the interactive console. Understanding its internals enables developers to customize or rewrite a debugger to suit specific needs, and many IDEs (e.g., PyCharm, VS Code) have built their own debuggers on top of these concepts. MNN Workbench leverages the native pdb to provide efficient side‑car debugging for mobile AI workloads.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
