Debugging with pdb

by Aaron Yong
for EdmontonPy

Outline

  • What is debugging?
  • Why do we debug?
  • What is pdb?
  • Basic pdb usage

What is debugging?

From the Oxford dictionary:

The process of identifying and removing errors from computer hardware or software.

 

Anecdotal definition:

What I resort to when just looking at the code and the problem doesn't result in a root cause.

Why do we debug?

  1. Gain a better understanding of code at runtime
  2. Identify root causes of elusive problems
  3. Remove the underlying bug in our code/system

What is pdb?

  • pdb: The Python Debugger
  • Command line debugging
  • Built-in module with every Python distribution

Basic pdb usage

  • Our buggy script
  • Invoking the debugger
  • Common pdb commands

Our buggy script

def sum_of_list(num_list):
    """
    Toy implementation of summing a list of numbers.  Unfortunately, it's
    crashing when we run it.
    """
    sum = 0

    # In the real world, use the sum() function instead.
    for i in num_list:
        sum += step_in(i)

    print(f"And the sum is... {sum}")


def step_in(num):
    """
    I don't do anything, I'm just here to demonstrate stepping in.
    """
    return num


if __name__ == "__main__":
    sum_of_list([2, 3, 5, "7", 9, 11, "13", 17])

What happens if we run it?

aaron@compy-386:~/pdb-tutorial $ python buggy_function.py

Traceback (most recent call last):
  File "buggy_function.py", line 25, in <module>
    sum_of_list([2, 3, 5, "7", 9, 11, "13", 17])
  File "buggy_function.py", line 12, in sum_of_list
    sum += step_in(i)
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

aaron@compy-386:~/pdb-tutorial $ 

It might be obvious what the problem is, but let's use it to learn pdb!

Invoking the debugger

There are two ways to invoke pdb:

  1. From your terminal

    python -m pdb <your_script>.py
  2. Setting a breakpoint inside your source code
for i in num_list:
    breakpoint()  # pdb will pause here
    sum += step_in(i)

Personally, I use the 2nd method 99% of the time.

import pdb; pdb.set_trace() still works if you prefer typing more.

Remember to delete breakpoints before deploying to production 😉

After invoking pdb

aaron@compy-386$ python buggy_function.py
> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) 
File being debugged buggy_function.py
Current line we're paused on 13
Our variable scope/context [>] sum_of_list()
Code on paused line [->] sum += step_in(i)

We are provided with lots of information:

Basic pdb commands

Command Summary
q Quits pdb and aborts execution
p <expression> Prints the given expression
pp <expression> Pretty-prints the expression
l
 
Show 11 lines of source code around the current line
n Execute the current line (step over)
s Execute and step into a function
r Continue execution until current function returns (step out)
c Continue execution until next breakpoint

q(uit)

Exit pdb and abort program execution.

 

When working on web projects, this terminates an HTTP connection.

aaron@compy-386$ python buggy_function.py
> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) q
Traceback (most recent call last):
  File "buggy_function.py", line 26, in <module>
    sum_of_list([2, 3, 5, "7", 9, 11, "13", 17])
  File "buggy_function.py", line 13, in sum_of_list
    sum += step_in(i)
  File "buggy_function.py", line 13, in sum_of_list
    sum += step_in(i)
  File "/Users/aaron/.pyenv/versions/3.8.1/lib/python3.8/bdb.py", line 88, in trace_dispatch
    return self.dispatch_line(frame)
  File "/Users/aaron/.pyenv/versions/3.8.1/lib/python3.8/bdb.py", line 113, in dispatch_line
    if self.quitting: raise BdbQuit
bdb.BdbQuit

aaron@compy-386$

p <expr>

Prints the expression.

 

An expression can be a single variable.

 

It can also be a comma-separated list of variables.
pdb returns a tuple!

aaron@compy-386$ python buggy_function.py 
> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) p sum
0
(Pdb) p sum, i
(0, 2)
(Pdb) locals()
{'num_list': [2, 3, 5, '7', 9, 11, '13', 17], 'sum': 0, 'i': 2}
(Pdb) 

Pro-tip:

The pdb prompt is also a Python interpreter!

pp <expr>

Pretty-prints the expression.

 

Can be useful when variables hold complex data structures.

(Pdb) p locals()
{'num_list': [2, 3, 5, '7', 9, 11, '13', 17], 'sum': 0, 'i': 2}
(Pdb) pp locals()
{'i': 2, 'num_list': [2, 3, 5, '7', 9, 11, '13', 17], 'sum': 0}

Pro-tip:

Use p and pp to safely interpret your expressions

l(ist) [first, [last]]

Shows 11 lines of source code around the current stoppage point.

 

Specifying a number for [first] shows 11 lines around that line.

Adding [last] returns code within that range.

> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) l
  8  	    sum = 0
  9  	
 10  	    # In the real world, use the sum() function instead.
 11  	    for i in num_list:
 12  	        breakpoint()
 13  ->	        sum += step_in(i)
 14  	
 15  	    print(f"And the sum is... {sum}")
 16  	
 17  	
 18  	def step_in(num):
(Pdb) 

n(ext)

Continues execution until the next line in your current function.

When looping, "next line" is the start of a new iteration.

Pro-tip: this can get tiring if looping over several items.  I won't cover it, but unt(il) can help you get out of these situations.

 

This is known as "stepping over" your code because the debugger does not pause inside step_in(i), it just runs it.

> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) p sum, i
(0, 2)
(Pdb) n
> ~/pdb-tutorial/buggy_function.py(11)sum_of_list()
-> for i in num_list:
(Pdb) 

s(tep)

Executes the current line then pauses at a function call.

 

If used on a line without a function call, it's the same as n(ext).

> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) s
--Call--
> ~/pdb-tutorial/buggy_function.py(18)step_in()
-> def step_in(num):
(Pdb) n
> ~/pdb-tutorial/buggy_function.py(22)step_in()
-> return num
(Pdb) 

r(eturn)

Executes until the current function returns (or crashes).

 

Note the return value is printed!

aaron@compy-386$ python buggy_function.py
> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) s
--Call--
> ~/pdb-tutorial/buggy_function.py(18)step_in()
-> def step_in(num):
(Pdb) r
--Return--
> ~/pdb-tutorial/buggy_function.py(22)step_in()->2
-> return num
(Pdb) 
aaron@compy-386$ python buggy_function.py
> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) r
> ~/pdb-tutorial/buggy_function.py(13)sum_of_list()
-> sum += step_in(i)
(Pdb) 

We tried to "step out" of sum_of_list() but pdb found our breakpoint inside the loop.

c(ontinue)

Continues program execution, pausing only for breakpoints.

aaron@compy-386$ python buggy_function.py
> ~/pdb-tutorial/buggy_function.py(12)sum_of_list()
-> for i in num_list:
(Pdb) l
  7  	    """
  8  	    sum = 0
  9  	    breakpoint()
 10  	
 11  	    # In the real world, use the sum() function instead.
 12  ->	    for i in num_list:
 13  	        sum += step_in(i)
 14  	
 15  	    print(f"And the sum is... {sum}")
 16  	
 17  	
(Pdb) c
Traceback (most recent call last):
  File "buggy_function.py", line 26, in <module>
    sum_of_list([2, 3, 5, "7", 9, 11, "13", 17])
  File "buggy_function.py", line 12, in sum_of_list
    for i in num_list:
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

aaron@compy-386$ 

Note that I moved the breakpoint out of the loop.

In our case, the script crashes.

Putting it all together...

Learn more pdb

There are many more commands that will make your debugging life easier!

Thank you!

Questions/Comments?

Made with Slides.com