🕯️ 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.
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.
| Command (short) | Command (full) | What It Does |
|---|---|---|
| l | list | Show current line and surrounding code (11 lines) |
| n | next | Execute current line and stop at next line (step over) |
| s | step | Step into function calls (enter the function) |
| c | continue | Continue execution until next breakpoint |
| p | Print value of expression: p variable | |
| pp | pp | Pretty-print value (better for lists, dicts) |
| q | quit | Quit the debugger and exit the program |
| w | where | Show call stack (which function called which) |
| u | up | Move up one frame in call stack |
| d | down | Move down one frame in call stack |
| b | break | Set a breakpoint: b 15 or b function_name |
| cl | clear | Clear breakpoints |
| ! | ! | Execute Python code: !x = 10 |
| h | help | Show help for a command |
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)
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.
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)
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).
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]
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)
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)
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
- 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
- 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.