Introduction

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

 

Importance of Debugging

Debugger is very important for all programming languages. Sometimes while writing large program, during execution program can misbehave. It may be difficult to trace the exact point where the program is misbehaving. So, debugger can be used to find the such point by executing the program line by line. The objective of 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 are present and point where the program is not working smoothly due to which there will be mess while handling program.

Useful commands in pdb module:

Command What function do they perform
args(a) gives list of arguments of current function
continue(c) creates a breakpoint in program
help(h) provides list of commands
jump(j) jump to the next line to be executed
list (l) gives you the line in which program currently is
p expression print the value of 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 un stack frame
down(d) move one level down to stack frame
breakpoint (b) show all breakpoint with number
<ENTER> key execute last command
unt(il) execute until the next line with number greater than 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, python terminal was used and gave the command as python -m pdb filename.py . We can see there was error when 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 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 about where we should start debugging after exception had occurred.

 

Run mode

In this mode, some expression is passed to run() function of pdb module to see it generates 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 pdb module didn't threw error while executing 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 mode 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 first line of code by default which is not so efficient. This trace mode can start debugging from any line of code using 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 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 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 is no errors in program. In such condition, debugger plays a vital role to correct the logic by inspection 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, function should return "fizz", if number is divisible by 5 the function should return "Buzz", if the number divisible by both 3 & 5 the function should return "fizzbuzz" and if the number is neither divisible by 5 nor 3 function 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 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 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 next condition. Since condition matched it returned "buzz" which is correct. 

Now, when 15 is passed to function, it should matched the third condition as 15 is divisible by both 3 and 5 and should return "fizzbuzz". But as output seen earlier, program didn't reaced to 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 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 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 debugger can be used in detecting errors and also examining the flow of program. Sometimes there can be some error in logic implementation but not syntactic error, debugger guides programmer to look through the code, see how program is reacting, how variables are changing, how flow control is changing and many more. Programmer can set 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 error and inspecting the flow of program.

 

Reference

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

Happy learning :-)