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)
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
(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
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
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
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
> /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
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.
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
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)
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
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.