Learn to debug a Python program using PDB


Reading time: 35 minutes

Debugging is the process of removing errors from your code. No, I am not talking about syntactical errors (we have compilers for that), I am talking about logical errors. Knowing tools like pdb can save you hours of head scratching and frustration when your code is not working as intended behavior or crashes unexpectedly (happens all the time).

Debugging is like God Mode where we can go through the program execution at human speed i.e., line by line. It allows us inspect any variable and even change their values mid execution. I often feel like Neo from the Matrix whenever I debug a program.

In this article, we will learn to:

  • Use Python's inbuilt debugger, PDB and do various things with it
  • Use PDB to find a bug in a real program and resolve it

How to access the debugger?

Python has an inbuilt module, conveniently named pdb which we can import in our programs to access the debugging powers. After that call the set_trace() function just before the line from where you want to start debugging. This will halt your program execution and transfer control to the pdb prompt.

>>> import pdb
>>> pdb.set_trace()
--Return--
> <stdin>(1)<module>()->None
(Pdb) 

Notice how the prompt changed from >>> to (Pdb). This means that pdb is ready to take in commands. Also the last second cryptic looking line > <stdin>(1)<module>()->None shows location in the program pdb was called from. Here we are typing directly into the terminal hence it shows standard input line 1.

Alternatively, you can simply call the breakpoint() function. It has the same effect of importing pdb and calling set_trace().

>>> breakpoint() # only works in python 3.7+
--Return--
> <stdin>(1)<module>()->None
(Pdb) 

If you don't want to import pdb module inside your program you can get into pdb by running this command from the terminal.

[user@host]$ python -m pdb testfile.py
> /run/media/user/test.py(1)<module>()
-> import os, pyperclip
(Pdb) 

Here testfile.py is the name of the file you want to debug. In this case the debugger will halt at start of the program. Hence we see the first line of the program in the output (yet to be executed) which in this case happens to be an import statement.

Overview of most important commands

View and change the value of a variable

For printing value of a variable we use p or pp command.

(Pdb) pp a
23
(Pdb) pp b
*** NameError: name 'b' is not defined
(Pdb) pp q
['I',
 'often',
 'feel',
 'like',
 'Neo']
(Pdb) !q = "random"
(Pdb) pp q
'random'
(Pdb) 

Here we can clearly see that variable a contains an integer value 23. Variable b is not defined yet and variable q is an array where each element is a string. The difference between p and pp is that pp formats the output so it is more readable like each element of q is printed in new line whereas p would have printed them in a single line. At last we change the value of q. The exclamation mark at the start is to ensure that the variable is not confused with any other pdb command.

View the source code

We can use l or ll commands to give some context like where are we in the program lines of code around current line. The l command will only print 11 lines if called without any arguments while ll prints the whole source code or function definition if the program is currently inside a function.

(Pdb) l
7  	  choice = 'some-random-topic'
8  	  print('Which domain do you want to read today?')
9  	  while choice not in list:
10        print("Enter 'list' to see the list of topics.")
11        choice = input('Enter your choice: ')
12  ->	  if choice == 'list':
13  	      print()
14  	      for i in list:
15  	          print(i)
16  	      print()
17  	  elif choice not in list:
(Pdb) 

Move forward in execution of code

For this we use n or s command. Key difference is that next (n) will not go into a function while step (s) will step into the function call line by line.

To understand this better let's consider this program. Notice how we are importing and calling set_trace() in one line. Go on and save this file as test.py.

# demonstration of pdb

def complexCalc(a, b, c):
        d = a + b
        e = b * c
        f = c - a
        result = d + e + f
        return result

import pdb; pdb.set_trace()
print("Calling the function")
answer = complexCalc(1, 2, 3)
print(answer)

The output will look something like this when you run the program.

> /run/media/user/test.py(11)<module>()
-> print("Calling the function")
(Pdb) n
Calling the function
> /run/media/user/test.py(12)<module>()
-> answer = complexCalc(1, 2, 3)
(Pdb) 
> /run/media/user/test.py(13)<module>()
-> print(answer)
(Pdb) 
11
--Return--
> /run/media/user/test.py(13)<module>()->None
-> print(answer)
(Pdb) 

Notice how here we used n for the first time and just kept pressing enter at subsequent prompts (pdb will execute previous command if you press enter). Now let's try entering s.

> /run/media/user/test.py(11)<module>()
-> print("Calling the function")
(Pdb) s
Calling the function
> /run/media/user/test.py(12)<module>()
-> complexCalc(1, 2, 3)
(Pdb) 
--Call--
> /run/media/user/test.py(3)complexCalc()
-> def complexCalc(a, b, c):
(Pdb) 
> /run/media/user/test.py(4)complexCalc()
-> d = a + b
(Pdb) 
> /run/media/user/test.py(5)complexCalc()
-> e = b * c
(Pdb) pp d
3
(Pdb) s
> /run/media/user/test.py(6)complexCalc()
-> f = c - a
(Pdb) pp e
6
(Pdb) s
> /run/media/user/test.py(7)complexCalc()
-> result = d + e + f
<-- Rest of output skipped -->

The difference between n and s must be clear by now.

Continue execution normally

Whenever you are done with debugging you can run c to continue the execution of program normally (unless it encounters a breakpoint). You can even use q to quit out of pdb.

Set and remove breakpoints

To set a breakpoint use b command. Syntax looks like b filename:lineno or b filename.function. Entering b without any arguments simply shows current breakpoints.

> /run/media/user/test.py(3)<module>()
-> def complexCalc(a, b, c):
(Pdb) ll
  1  	# demonstration of pdb
  2  	
  3  ->	def complexCalc(a, b, c):
  4  		d = a + b
  5  		e = b * c
  6  		f = c - a
  7  		result = d + e + f
  8  		return result*
  9  	
 10  	print("Calling the function")
 11  	answer = complexCalc(1, 2, 3)
 12  	print(answer)
(Pdb) b test.py:11
Breakpoint 1 at /run/media/user/test.py:11
(Pdb) c
Calling the function
> /run/media/user/test.py(11)<module>()
-> answer = complexCalc(1, 2, 3)
(Pdb) s
--Call--
> /run/media/user/test.py(3)complexCalc()
-> def complexCalc(a, b, c):
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /run/media/user/test.py:11
	breakpoint already hit 1 time
(Pdb) 

Here first we use ll to see some lines around the current one. Then we create a breakpoint at line 11. After that we use c to continue until line 11 is reached. We can use clear to clear all breakpoints.

Getting help

There are a lot more commands in pdb that we can't cover here and it may be hard to keep track of all of them. Help command (h) will show you a list of all pdb commands.

(Pdb) h

Documented commands (type help <topic>):
========================================
EOF    c          d        h         list      q        rv       undisplay
a      cl         debug    help      ll        quit     s        unt      
alias  clear      disable  ignore    longlist  r        source   until    
args   commands   display  interact  n         restart  step     up       
b      condition  down     j         next      return   tbreak   w        
break  cont       enable   jump      p         retval   u        whatis   
bt     continue   exit     l         pp        run      unalias  where    

Miscellaneous help topics:
==========================
exec  pdb

(Pdb) h c
c(ont(inue))
        Continue execution, only stop when a breakpoint is encountered.
(Pdb) 

You can even use the help() function of pdb library to get a manual page like more verbose documentation.

Fixing a real program

Here is a simple program which I wrote to tell whether your BMI (ratio of mass and weight squared) is normal or not normal.

m = float(input("Enter weight in kg: "))
h = float(input("Enter height in meters: "))
bmi = m / h*h
# normal range by wikipedia
if bmi >= 18.5 and bmi <= 24.9:
        print("Normal")
else:
        print("Not normal")

But this code is printing wrong values. For example if we punch in my brother's info who has normal a BMI the program outputs Not normal.

Enter weight in kg: 56
Enter height in meters: 1.5
Not normal

Let's try to debug this. First we will run the program through pdb.

[user@host]$ python -m pdb bmi.py
> run/media/user/bmi.py(1)<module>()
-> m = float(input("Enter weight in kg: "))
(Pdb) 

We will go on and enter our weight and height. Hence stepping through next two lines of code.

(Pdb) n
Enter weight in kg: 56
> run/media/user/bmi.py(2)<module>()
-> h = float(input("Enter height in meters: "))
(Pdb) n
Enter height in meters: 1.5
> run/media/user/bmi.py(3)<module>()
-> bmi = m / h*h
(Pdb) 

At this point it is probably a good idea to check that the values stored in variables are same as we expect.

(Pdb) pp m, h
(56.0, 1.5)
(Pdb) n
> run/media/user/bmi.py(5)<module>()
-> if bmi >= 18.5 and bmi <= 24.9:
(Pdb)

Variables m and h contain 56.0 and 1.5 respectively which is what we entered. I also stepped through next line where we calculate bmi. Now let's quickly check it's value just out of curiosity.

(Pdb) pp bmi
56.0
(Pdb) 

Whoa! Value of bmi is same as m? Looks like we found our bug. Can you figure it out? On a closer inspection we are just dividing and multiplying m by h once and hence canceling out it's effect. Remember that python evaluates the expression left to right if two operators of same precedence are encountered. Putting a parenthesis around h*h will fix it.

Let's update the value of bmi and continue normal execution to see if it fixes our program and there are no bugs in our conditional logic.

(Pdb) bmi = m / (h*h)
(Pdb) pp bmi
24.88888888888889
(Pdb) c
Normal
The program finished and will be restarted
> run/media/user/bmi.py(1)<module>()
-> m = float(input("Enter weight in kg: "))
(Pdb) exit
[user@host]$ 

Congratulations! We just debugged our first program. Now it outputs Normal as expected. Notice how pdb restarts the program automatically after it has been executed. Type exit/quit to quit out of pdb. And don't forget to add the changes in actual source code.

Alternatives to pdb?

Pdb is very handy but it is a little arcane if you are working on a big project spanning across multiple files because pdb does not provides any visual clues.

To overcome this you can install pudb using pip. It has a graphical interface and slightly less learning curve. Many IDEs like IDLE, VScode, Thonny, Pycharm nowadays also have graphical debuggers built into them.

If you are not sure of the flow of execution of program or how a variable is changing values you can simply use a print statement here and there. Just make sure to remove them after the bug is solved.

Debugging should not be limited by a tool or piece of software. Tools like pdb make our life a little easier by holding our hands during the process.