0%

Debug Like a Pro: Interactive Debugging with pdb

Step through your code, inspect variables, find bugs faster. Replace print() with the professional Python debugger.

You have debugged with print(). Add a print. Run the code. See the output. Remove the print. Add another. This works for small scripts. But for complex bugs? For loops that run hundreds of times? For code that fails only in production? print() is slow. It requires restarting the program. It cannot inspect variables interactively. It cannot step through code line by line. The Python Debugger (pdb) solves these problems. It lets you pause execution, examine variables, step through lines, and continue—all interactively. It is built into Python. No installation needed. This tutorial teaches you to use pdb like a professional. You will learn to set breakpoints, step through code, inspect variables, and find bugs faster than ever before.

🕯️ Magic Note

The Python debugger has been part of the standard library since Python 1.4 (1996). It is inspired by gdb, the GNU debugger for C. Learning pdb will make you a more efficient and effective debugger.

Your First Debugging Session
The simplest way to start debugging: add breakpoint() to your code.

Python (buggy_code.py)

def calculate_average(numbers):

total = sum(numbers)

count = len(numbers)

# The bug: dividing by wrong variable

return total / total # Should be total / count!

def main():

scores = [85, 92, 78, 90, 88]

breakpoint() # Debugger will stop here

avg = calculate_average(scores)

print(f”Average: {avg}”)

if __name__ == “__main__”:

main()

Running the Code (pdb session)

$ python buggy_code.py

–Return–

-> breakpoint()

(Pdb)

🕯️ Magic Note

The breakpoint() function was added in Python 3.7. It is a built-in alias for pdb.set_trace(). It pauses execution exactly where you place it and drops you into an interactive debugger session.

Essential pdb Commands
Commands you will use most frequently.
Command (short)Command (full)What It Does
llistShow current line and surrounding code (11 lines)
nnextExecute current line and stop at next line (step over)
sstepStep into function calls (enter the function)
ccontinueContinue execution until next breakpoint
pprintPrint value of expression: p variable
ppppPretty-print value (better for lists, dicts)
qquitQuit the debugger and exit the program
wwhereShow call stack (which function called which)
uupMove up one frame in call stack
ddownMove down one frame in call stack
bbreakSet a breakpoint: b 15 or b function_name
clclearClear breakpoints
!!Execute Python code: !x = 10
hhelpShow help for a command
Stepping Through Code (n vs s)
Understand the difference between next and step.

Python

def multiply(a, b):

result = a * b

return result

def calculate(a, b):

print(“Calculating…”)

value = multiply(a, b) # `next` jumps over this line

return value + 10

breakpoint()

result = calculate(5, 3)

Debugger Session (using `n` – step over)

(Pdb) n

-> result = calculate(5, 3)

(Pdb) n

–Call–

-> def calculate(a, b):

(Pdb) n

-> print(“Calculating…”)

(Pdb) n

Calculating…

-> value = multiply(a, b) # `n` executes the whole multiply function without entering it

(Pdb) n

-> return value + 10

(Pdb) p value

15

Debugger Session (using `s` – step into)

(Pdb) s

-> value = multiply(a, b) # We are here

(Pdb) s

–Call–

-> def multiply(a, b):

(Pdb) s

-> result = a * b

(Pdb) s

-> return result

(Pdb) p a, b

(5, 3)

(Pdb) p result

15

(Pdb) s

–Return–

-> return result

(Pdb)

💡 Use next (n) when you trust a function. Use step (s) when you want to debug inside a function. next executes the whole function call; step enters it.
Inspecting Variables (p, pp, display)
Look at variable values during debugging.

Python

def process_users(users):

total_age = 0

for user in users:

# breakpoint() here

total_age += user[“age”]

return total_age

users = [{“name”: “Ali”, “age”: 25}, {“name”: “Sara”, “age”: 30}]

breakpoint()

result = process_users(users)

Debugger Session (inspecting variables)

(Pdb) l

7 def process_users(users):

8 total_age = 0

9 for user in users:

10 breakpoint()

11 -> total_age += user[“age”]

12 return total_age

(Pdb) p user

{‘name’: ‘Ali’, ‘age’: 25}

(Pdb) pp user

{‘name’: ‘Ali’, ‘age’: 25}

(Pdb) p total_age

0

(Pdb) p user[“age”]

25

(Pdb) p locals()

{‘user’: {‘name’: ‘Ali’, ‘age’: 25}, ‘total_age’: 0, ‘users’: […]}

(Pdb) !total_age = 100 # Modify variable

(Pdb) p total_age

100

🕯️ Magic Note

Use pp (pretty-print) for nested data structures like dictionaries and lists. It formats output nicely, unlike p which prints everything on one line.

Setting Breakpoints (b)
Set breakpoints without modifying code.

Python (debug_me.py)

def first_function():

print(“Starting first function”)

result = 10 + 5

print(f”First result: {result}”)

return result

def second_function():

print(“Starting second function”)

value = 20 * 3

print(f”Second value: {value}”)

return value

def main():

a = first_function()

b = second_function()

print(f”Result: {a + b}”)

if __name__ == “__main__”:

main()

Debugger Session (setting breakpoints)

$ python -m pdb debug_me.py

(Pdb) b first_function # Break at function

Breakpoint 1 at debug_me.py:1

(Pdb) b 14 # Break at line 14

Breakpoint 2 at debug_me.py:14

(Pdb) b # List all breakpoints

Num Type Disp Enb Where

1 breakpoint keep yes at debug_me.py:1

2 breakpoint keep yes at debug_me.py:14

(Pdb) c # Continue to breakpoint 1

-> def first_function():

(Pdb) c # Continue to breakpoint 2

Starting first function

First result: 15

-> b = second_function()

(Pdb)

Conditional Breakpoints
Stop only when a condition is met.

Python

def find_errors(data):

for i, item in enumerate(data):

if item < 0:

print(f”Negative found at index {i}: {item}”)

processed = item * 2

# We want to break only when item is negative

return data

data = [10, -5, 20, -3, 15, -8]

result = find_errors(data)

Debugger Session (conditional breakpoint)

$ python -m pdb conditional_break.py

(Pdb) b 3, item < 0 # Break at line 3 when item is negative

Breakpoint 1 at conditional_break.py:3

(Pdb) c

-> if item < 0: # Stopped at i=1 (item=-5)

(Pdb) p i, item

(1, -5)

(Pdb) c

-> if item < 0: # Stopped at i=3 (item=-3)

(Pdb) p i, item

(3, -3)

🕯️ Magic Note

Conditional breakpoints are extremely powerful for debugging loops. Instead of stopping at every iteration, you stop only when something interesting happens (e.g., value out of range, specific index, or error condition).

Post-Mortem Debugging
Debug after an exception occurs.

Python (crashing_code.py)

def divide_list(numbers, divisor):

results = []

for num in numbers:

results.append(num / divisor)

return results

data = [10, 20, 0, 40, 50] # Zero in data!

result = divide_list(data, 2)

Post-Mortem Session

$ python -m pdb crashing_code.py

(Pdb) c

ZeroDivisionError: division by zero

-> results.append(num / divisor)

(Pdb) p num, divisor

(0, 2)

(Pdb) w # Where – show call stack

/crashing_code.py(7)<module>()

-> result = divide_list(data, 2)

/crashing_code.py(4)divide_list()

-> results.append(num / divisor)

(Pdb) u # Up one level

-> result = divide_list(data, 2)

(Pdb) p data

[10, 20, 0, 40, 50]

Practical Example: Debugging a Recursive Function
Step through recursive calls to understand their behavior.

Python

def factorial(n):

print(f”Calling factorial({n})”)

if n <= 1:

return 1

return n * factorial(n – 1)

breakpoint()

result = factorial(5)

print(result)

Debugger Session (tracing recursion)

(Pdb) s

-> result = factorial(5)

(Pdb) s

–Call–

-> def factorial(n):

(Pdb) s

-> print(f”Calling factorial({n})”)

(Pdb) s

Calling factorial(5)

-> if n <= 1:

(Pdb) s

-> return n * factorial(n – 1)

(Pdb) s

–Call–

-> def factorial(n):

(Pdb) p n

4

(Pdb)

Advanced: Using pdb in Jupyter Notebooks
Debugging in Jupyter requires special handling.

Python (Jupyter Cell)

import pdb

def buggy_function(x, y):

result = x + y

result = result * (x – y)

return result / 0 # Division by zero

# Option 1: Post-mortem with %debug magic

%debug

# Option 2: Set breakpoint with pdb.set_trace()

import pdb; pdb.set_trace()

buggy_function(10, 5)

💡 In Jupyter, use %debug after an exception for post-mortem debugging. Use import pdb; pdb.set_trace() for interactive debugging (though breakpoint() works in Python 3.7+).
Debugging from the Command Line
Run your script directly under pdb control.

Bash

# Run script under debugger (stops at first line)

python -m pdb my_script.py

# Run with breakpoint in code (stops at breakpoint())

python my_script.py

# Debug a crashed script (post-mortem)

python -m pdb -c continue my_script.py

Common pdb Mistakes
  • Forgetting to use `s` to step into functions (using `n` instead)
  • Typing variable names without `p` (pdb tries to execute them as commands)
  • Using `p` for assignments (use `!` instead: `!x = 10`)
  • Not knowing the difference between `q` (quit) and `c` (continue)
  • Leaving `breakpoint()` in production code

Common Mistake Example

(Pdb) x = 10 # Wrong: pdb thinks ‘x’ is a command

*** NameError: name ‘x’ is not defined

(Pdb) !x = 10 # Correct: execute Python code

(Pdb) p x

10

Check Your Understanding
  • What command advances to the next line but does not enter function calls?
  • How do you set a breakpoint at line 42 in your code?
  • What is the difference between `p` and `pp`?
  • How do you continue execution after a breakpoint?
  • What command shows the call stack?
  • How do you modify a variable’s value during debugging?

⚡ Whisper

Print is a crutch. pdb is a tool. Print tells you what happened after the fact. pdb shows you what is happening right now. You can pause. You can inspect. You can step. You can change. You can see the call stack. You can set breakpoints that trigger only when conditions are met. This is not just debugging. This is understanding. Your code becomes transparent. Bugs that took hours with print take minutes with pdb. Learn the commands. Practice on broken code. Make breakpoint() your friend. Then remove them before commit. Debugging is a skill. pdb is your instrument. Play it well.

Related posts