Debugging in Python | pdb module

Introduction

The debugger is a program that helps us to find what is going on in our program. You can use the debugger to execute each line of code, print variables in each step, break execution at any line, again start execution, stop execution and repeat the same steps until you find the bug in the program.

 

Importance of Debugging

The debugger is very important for all programming languages. Sometimes while writing a large program, during execution program can misbehave. It may be difficult to trace the exact point where the program is misbehaving. So, the debugger can be used to find such points by executing the program line by line.

The objective of the debugger is to find the error in the program and fix it.  Debugger helps you to understand the actual program flow and locate the point where the error is present and the point where the program is not working smoothly due to which there will be a mess while handling the program.

Useful commands with their functions in pdb module:

args(a) ⇒ gives a list of arguments of the current function

continue(c) ⇒ creates a breakpoint in the program

help(h) ⇒  provides a list of commands

jump(j) ⇒ jump to the next line to be executed

list(l) ⇒ gives you the line in which the program currently is

p expression ⇒ print the value of the expression

pp expression ⇒ pretty prints the values of expression

quit(q) ⇒ terminates the program

return(r) ⇒ continue execution until the current function returns

next(n) ⇒ execute next line

step(s) ⇒ execute and step in function

up(u) ⇒ move one level up to stack frame

down(d) ⇒ move one level down to stack frame

breakpoint(b) ⇒ show all breakpoint with the number

<ENTER> key ⇒ execute the last command

unt(il) ⇒ execute until the next line with a number greater than the current reached

 

The different ways of getting the pdb prompt

Script mode

In this mode, the debugger steps through the entire pdb script.

print("This is debugging in python\n")
print("Let's start\n")

def div(a,b):
	result = a/b
	return result
	
print(div(5, 0))

Output

/storage/emulated/0 $ python -m pdb newfile.py
> /storage/emulated/0/newfile.py(1)<module>()
-> print("This is debugging in python\n")
(Pdb) l
  1  -> print("This is debugging in python\n")
  2     print("Let's start\n")
  3                                                           4     def div(a,b):                                         5             result = a/b
  6             return result                                 7
  8     print(div(5, 0))                                    [EOF]                                                       (Pdb) n
This is debugging in python
> /storage/emulated/0/newfile.py(2)<module>()
-> print("Let's start\n")
(Pdb) l
  1     print("This is debugging in python\n")
  2  -> print("Let's start\n")
  3
  4     def div(a,b):
  5             result = a/b
  6             return result
  7
  8     print(div(5, 0))
[EOF]
(Pdb) n
Let's start

> /storage/emulated/0/newfile.py(4)<module>()
-> def div(a,b):
(Pdb) l
  1     print("This is debugging in python\n")
  2     print("Let's start\n")
  3
  4  -> def div(a,b):
  5             result = a/b
  6             return result
  7
  8     print(div(5, 0))
[EOF]
(Pdb) n
> /storage/emulated/0/newfile.py(8)<module>()
-> print(div(5, 0))
(Pdb) l
  3
  4     def div(a,b):
  5             result = a/b
  6             return result
  7
  8  -> print(div(5, 0))
[EOF]
(Pdb) n
ZeroDivisionError: division by zero
> /storage/emulated/0/newfile.py(8)<module>()
-> print(div(5, 0))
(Pdb) q
/storage/emulated/0 $

Analysis of output of above code, list(l) command is used to printing the source code pointing which line was currently running and next(n) command for execution of code line by line.

To run this code, the python terminal was used and gave the command as python -m pdb filename.py. We can see there was an error when the print(div(5,0)) statement got executed. Quit(q) command was used to get out of the program.

 

Postmortem mode

This mode is used after an uncaught exception has been raised

def add(a,b):
	result = a+b
	return result
	
print(add(5, "b"))

Output

/storage/emulated/0 $ python -i newfile.py
Traceback (most recent call last):
  File "newfile.py", line 5, in <module>
    print(add(5, "b"))
  File "newfile.py", line 2, in add
    result = a+b
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> import pdb
>>> pdb.pm()
> /storage/emulated/0/newfile.py(2)add()
-> result = a+b
(Pdb)

This mode tells us where we should start debugging after an exception had occurred.

 

Run mode

In this mode, some expression is passed to run() function of pdb module to see it generates an error or not

print(6+7)
def add(a,b):
	result = a+b
	return result
	
print(add(5, 6))

Output

/storage/emulated/0 $ python -i newfile.py
Debugger
13
Traceback (most recent call last):
  File "newfile.py", line 7, in <module>
    print(aad(5, 6))
NameError: name 'aad' is not defined
>>> import pdb
>>> pdb.run('print(5+6)')
> <string>(1)<module>()
(Pdb) s
11
--Return--
> <string>(1)<module>()->None
(Pdb) q
>>> pdb.run('print(aad(5,6))')
> <string>(1)<module>()->None
(Pdb) s
NameError: name 'aad' is not defined
> <string>(1)<module>()->None
(Pdb)

As we can see the run() function of the pdb module didn’t throw an error while executing the first statement but threw NameError while executing the statement calling the function because the function name was given incorrect while calling it.

 

Trace mode

All the modes till now are quite useful but not so efficient when we want to debug a large program. Execution of code is line by line and from the first line of code by default which is not so efficient. This trace mode can start debugging from any line of code using the set_trace() function.

import pdb
print("This is debugging in python")

class Calculator:
	def __init__(self, num1, num2):
		self.a = num1
		self.b = num2
		
	def add(self):
		result = self.a + self.b
		print("Sum: ", result)
		
	def sub(self):
		result = self.a - self.b
		print("Subtraction: ", result)
		
	def mul(self):
		print("multiplication: ", self.a*self.b)
		
	def div(self):
		pdb.set_trace()
		print("division: ", self.a/self.b)
	
cal = Calculator(6,0)
cal.div()

Output

This is debugging in python
> /home/dcoder/hh/main.py(22)div()
-> print("division: ", self.a/self.b)
(Pdb) l
 17  	  def mul(self):
 18  	    print("multiplication: ", self.a*self.b)
 19  	
 20  	  def div(self):
 21  	    pdb.set_trace()
 22  ->	    print("division: ", self.a/self.b)
 23  	
 24  	cal = Calculator(6,0)
 25  	cal.div()
[EOF]
(Pdb) n
ZeroDivisionError: division by zero
> /home/dcoder/hh/main.py(22)div()
-> print("division: ", self.a/self.b)
(Pdb)

When we divide 6 by 0 there will be an error. So we can use set_trace() function directly in div function of calculator class. If the above method had been used then we had to go line by line to trace the error which could be time-consuming.

Debugging is not only used to trace the error in the program but also used to inspect the program (how the program behaving, how the variables are changing). There could be the case when the program is syntactically correct but not logically. We may not get the desired output if there are no errors in the program. In such conditions, a debugger plays a vital role to correct the logic by inspecting of program.

To explain this, let’s take a problem related to fizzbuzz algorithm. The rules are: if a number passed to a function is divisible by 3, the function should return “fizz” if the number is divisible by 5 the function should return “Buzz” if the number is divisible by both 3 & 5 the function should return “fizzbuzz” and if the number is neither divisible by 5 nor 3 functions should return the same number passed as argument.

def fizz_buzz(num):
  if num % 3 == 0:
    return "fizz"
  elif num % 5 == 0:
    return "buzz"
  elif (num % 3 == 0) and (num % 5 == 0):
    return "fizzbuzz"
  else:
    return num
    
print(fizz_buzz(6))

print(fizz_buzz(10))

print(fizz_buzz(15))

Output

fizz
buzz
fizz

We got the correct output for 6 and 10. But when 15 is passed to function, it is returning “fizz” instead of “fizzbuzz”. Let’s inspect the program where is the logical error using pdb module

import pdb
def fizz_buzz(num):
  pdb.set_trace()
  if num % 3 == 0:
    return "fizz"
  elif num % 5 == 0:
    return "buzz"
  elif (num % 3 == 0) and (num % 5 == 0):
    return "fizzbuzz"
  else:
    return num
    
print(fizz_buzz(6))

print(fizz_buzz(10))

print(fizz_buzz(15))

Output

-> if num % 3 == 0:
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  ->	  if num % 3 == 0:
  5  	    return "fizz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0) and (num % 5 == 0):
  9  	    return "fizzbuzz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(5)fizz_buzz()
-> return "fizz"

When 6 is passed to the fizz_buzz function, it returns “fizz” which is correct. So let’s move ahead

(Pdb) n
fizz
> /home/dcoder/hh/main.py(15)<module>()
-> print(fizz_buzz(10))
(Pdb) l
 10  	  else:
 11  	    return num
 12  	
 13  	print(fizz_buzz(6))
 14  	
 15  ->	print(fizz_buzz(10))
 16  	
 17  	print(fizz_buzz(15))
[EOF]
(Pdb) n
> /home/dcoder/hh/main.py(4)fizz_buzz()
-> if num % 3 == 0:
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  ->	  if num % 3 == 0:
  5  	    return "fizz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0) and (num % 5 == 0):
  9  	    return "fizzbuzz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(6)fizz_buzz()
-> elif num % 5 == 0:
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  	  if num % 3 == 0:
  5  	    return "fizz"
  6  ->	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0) and (num % 5 == 0):
  9  	    return "fizzbuzz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(7)fizz_buzz()
-> return "buzz"
(Pdb)

When next(n) command is passed ‘print(fizz_buzz(10))’ got executed. We can see that the first condition is not matched so it moved to the next condition. Since the condition matched it returned “buzz” which is correct.

Now, when 15 is passed to function, it should match the third condition as 15 is divisible by both 3 and 5 and should return “fizzbuzz”. But as the output is seen earlier, the program didn’t reach the third condition. Let’s see what has actually happened

-> print(fizz_buzz(15))
(Pdb) l
 12  	
 13  	print(fizz_buzz(6))
 14  	
 15  	print(fizz_buzz(10))
 16  	
 17  ->	print(fizz_buzz(15))
[EOF]
(Pdb) n
> /home/dcoder/hh/main.py(4)fizz_buzz()
-> if num % 3 == 0:
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  ->	  if num % 3 == 0:
  5  	    return "fizz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0) and (num % 5 == 0):
  9  	    return "fizzbuzz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(5)fizz_buzz()
-> return "fizz"
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  	  if num % 3 == 0:
  5  ->	    return "fizz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0) and (num % 5 == 0):
  9  	    return "fizzbuzz"
 10  	  else:
 11  	    return num
(Pdb)

When ‘print(fizz_buzz(15))’ is executed, the first condition got matched and return “fizz” which is incorrect. The program didn’t check whether the number is divisible by both 5 & 3. It means the number passed to function should check whether it is divisible by both 3 and 5 or not at first then go to other conditions.

Let’s make a change and see the outcome

def fizz_buzz(num):
  if (num % 3 == 0) and (num % 5 == 0):
    return "fizzbuzz"
  elif num % 5 == 0:
    return "buzz"
  elif (num % 3 == 0):
    return "fizz"
  else:
    return num
    
print(fizz_buzz(6))

print(fizz_buzz(10))

print(fizz_buzz(15))

print(fizz_buzz(67))

Output

fizz
buzz
fizzbuzz
67

Here, we got the correct output now. The whole flow of the program is shown below

import pdb
def fizz_buzz(num):
  pdb.set_trace()
  if (num % 3 == 0) and (num % 5 == 0):
    return "fizzbuzz"
  elif num % 5 == 0:
    return "buzz"
  elif (num % 3 == 0):
    return "fizz"
  else:
    return num

pdb.set_trace()
print(fizz_buzz(6))

print(fizz_buzz(10))

print(fizz_buzz(15))

print(fizz_buzz(67))

Output

-> print(fizz_buzz(6))
(Pdb) n
> /home/dcoder/hh/main.py(4)fizz_buzz()
-> if (num % 3 == 0) and (num % 5 == 0):
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  ->	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(6)fizz_buzz()
-> elif num % 5 == 0:
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  ->	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(8)fizz_buzz()
-> elif (num % 3 == 0):
(Pdb) l
  3  	  pdb.set_trace()
  4  	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  ->	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
 12  	
 13  	pdb.set_trace()
(Pdb) n
> /home/dcoder/hh/main.py(9)fizz_buzz()
-> return "fizz"
(Pdb) n
--Return--
> /home/dcoder/hh/main.py(9)fizz_buzz()->'fizz'
-> return "fizz"
(Pdb) n
fizz
> /home/dcoder/hh/main.py(16)<module>()
-> print(fizz_buzz(10))
(Pdb) l
 11  	    return num
 12  	
 13  	pdb.set_trace()
 14  	print(fizz_buzz(6))
 15  	
 16  ->	print(fizz_buzz(10))
 17  	
 18  	print(fizz_buzz(15))
 19  	
 20  	print(fizz_buzz(67))
[EOF]
(Pdb) n
> /home/dcoder/hh/main.py(4)fizz_buzz()
-> if (num % 3 == 0) and (num % 5 == 0):
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  ->	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(6)fizz_buzz()
-> elif num % 5 == 0:
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  ->	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(7)fizz_buzz()
-> return "buzz"
(Pdb) n
--Return--
> /home/dcoder/hh/main.py(7)fizz_buzz()->'buzz'
-> return "buzz"
(Pdb) n
buzz
> /home/dcoder/hh/main.py(18)<module>()
-> print(fizz_buzz(15))
(Pdb) l
 13  	pdb.set_trace()
 14  	print(fizz_buzz(6))
 15  	
 16  	print(fizz_buzz(10))
 17  	
 18  ->	print(fizz_buzz(15))
 19  	
 20  	print(fizz_buzz(67))
[EOF]
(Pdb) n
> /home/dcoder/hh/main.py(4)fizz_buzz()
-> if (num % 3 == 0) and (num % 5 == 0):
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  ->	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(5)fizz_buzz()
-> return "fizzbuzz"
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  	  if (num % 3 == 0) and (num % 5 == 0):
  5  ->	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
--Return--
> /home/dcoder/hh/main.py(5)fizz_buzz()->'fizzbuzz'
-> return "fizzbuzz"
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  	  if (num % 3 == 0) and (num % 5 == 0):
  5  ->	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
fizzbuzz
> /home/dcoder/hh/main.py(20)<module>()
-> print(fizz_buzz(67))
(Pdb) n
> /home/dcoder/hh/main.py(4)fizz_buzz()
-> if (num % 3 == 0) and (num % 5 == 0):
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  ->	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(6)fizz_buzz()
-> elif num % 5 == 0:
(Pdb) l
  1  	import pdb
  2  	def fizz_buzz(num):
  3  	  pdb.set_trace()
  4  	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  ->	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
(Pdb) n
> /home/dcoder/hh/main.py(8)fizz_buzz()
-> elif (num % 3 == 0):
(Pdb) l
  3  	  pdb.set_trace()
  4  	  if (num % 3 == 0) and (num % 5 == 0):
  5  	    return "fizzbuzz"
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  ->	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  	    return num
 12  	
 13  	pdb.set_trace()
(Pdb) n
> /home/dcoder/hh/main.py(11)fizz_buzz()
-> return num
(Pdb) l
  6  	  elif num % 5 == 0:
  7  	    return "buzz"
  8  	  elif (num % 3 == 0):
  9  	    return "fizz"
 10  	  else:
 11  ->	    return num
 12  	
 13  	pdb.set_trace()
 14  	print(fizz_buzz(6))
 15  	
 16  	print(fizz_buzz(10))
(Pdb) n
--Return--
> /home/dcoder/hh/main.py(11)fizz_buzz()->67
-> return num
(Pdb) n
67
--Return--

 

Conclusion

This is how a debugger can be used in detecting errors and also examining the flow of the program. Sometimes there can be some error in logic implementation but not a syntactic error, debugger guides the programmer to look through the code, see how the program is reacting, how variables are changing, how flow control is changing, and many more.

Programmers can set a breakpoint at any line of code and inspect the flow to observe what is actually going on. So, debugging tool is very important for detecting errors and inspecting the flow of the program.

Reference

https://realpython.com/python-debugging-pdb/

Happy learning 🙂

Leave a Comment