Python Tutorials

Python debugging using pdb

Debugging code is a necessity, but still I have seen many developers using print() statements instead of powerful built-in pdb python debugger.

In this tutorial, we will see how to use pdb for debugging Python3 scripts and complex application code.

pdb can be used for debugging python code on all operating systems like Linux, Windows, MacOS etc. Hence, pdb is very useful for debugging python code in any kind of remote servers where we do not have access to GUI, therefore we can debug our Python code through commandline.

The most direct and the easiest way to use pdb is to import pdb and call set_trace() within your code on that particular line from where you want to start debugging.

Syntax:

import pdb; pdb.set_trace()

If you are using Python 3.7+ , you can write breakpoint() in place of above syntax:

breakpoint()

Example:

def add_num(num1,num2):
  print('we have reached inside add_num function')
  new_num = num1 + num2
  import pdb; pdb.set_trace()
  return new_num


There is another way to debug code with pdb which is called Post-mortem debugging. Let's assume you want to debug the code inside an used 3rd party library/module. You will have to open the source code of that library and introduce the pdb.set_trace() code, isn't it a tedious task?
For resolving this we can use post-mortem debugging.

Run the following command with the specific filename to start debugging:

python -m pdb <filename>.py

Note : here pdb is run as a python module because we are using '-m' in the command.


We'll come to post-mortem debugging later, now lets practically learn how to efficiently debug the code. We'll use the above add_num function for it.

example.py

def add_num(num1,num2):
  print('we have reached inside add_num function')
  new_num = num1 + num2
  import pdb; pdb.set_trace()
  return new_num

add_num(10, 15)


If we run the script with <python3 example.py>:

we have reached inside add_num function
> desktop\example.py(3)add_num()
->return new_num
(Pdb)


Points to consider :

Basic commands we can perform to debug our code:

Basic command 1 : p (print)
p lets you print the value of a variable.

We will be using this piece of code as example :

def add_num(num1,num2):
  print('we have reached inside add_num function')
  new_num = num1 + num2
  import pdb; pdb.set_trace()
  return new_num

add_num(10, 15)


Run the script:

we have reached inside add_num function
> desktop\example.py(3)add_num()
->return new_num
(Pdb) p new_num
(Pdb) 25.0

No more need of inserting print statements all over the code, amazing right? We can do a lot more things to investigate what is going on in current logic.

(Pdb) p new_num * 5
125
(Pdb) p new_num * 10
1250
(Pdb) p new_num + 10
35

If we again print the value of that variable we can see the value does not change after investigation.

(Pdb) p new_num
15


Basic command 2 : a (args)
To check the arguments passed in the function on which logic is applied, we can use - a (args):

(Pdb) a
num1 = 10
num2 = 15


Basic command 3 : whatis
It checks the data type of the variable. It is equivalent to type(argument) in Python code.

(Pdb) whatis num1
<class 'int'>
(Pdb) whatis new_num
<class 'int'>
(Pdb) type(new_num)
<class 'int'>

Note : All other python functions can also be called through pdb prompt, like we used type() in the above example.

Stepping forward in debug mode

Basic command 4 : n (next)

def get_address(id):
  return 'dummy-address'

def print_details(name, id):
  import pdb; pdb.set_trace()
  print('Name is : ', name)
  address = get_address(id)
  return address

print_details('getechready', 7)

Shell:

> desktop\example.py(2)print_details()
->print('Name is : ', name)

(Pdb) n
Name is : getechready
> desktop\example.py(3)print_details()
->address = get_address(id)

(Pdb) n
> desktop\example.py(4)print_details()
return address

(Pdb) n
--Return--
> desktop\example.py(5)print_details()->'dummy_address'

Bonus tip : When you have to go through many lines of code while debugging, you do not have to press 'n' everytime, after pressing 'n' one time pdb is smart enough to understand that next action will be same only, so you can just press "ENTER⏎" key after it.


Basic command 5 : s (step)

def get_address(id):
  return 'dummy-address'

def print_details(name, id):
  import pdb; pdb.set_trace()
  print('Name is : ', name)
  address = get_address(id)
  return address

print_details('getechready', 7)

Shell:

> desktop\example.py(2)print_details()
->print('Name is : ', name)

(Pdb) s
Name is : getechready
> desktop\example.py(3)print_details()
->address = get_address(id)

(Pdb) s
--Call--
> desktop\example.py(1)get_address()
-> def get_address()

(Pdb) s
> desktop\example.py(2)get_address()
return'dummy-address'

(Pdb) s
--Return--
> desktop\example.py(2)get_address()
return'dummy-address'

(Pdb) s
> desktop\example.py(8)get_address()
return address


Basic command 6 : ll (longlist)
ll command lists the code for current function or snippet of current frame is function is very long.
It is a very helpful command when you are dealing the large code base where you can easily get lost, just use ll to find out where you are.

Lets run the above code once again:

Shell:

> desktop\example.py(2)print_details()
->print('Name is : ', name)

(Pdb) ll
4  def print_details(name, id):
5    import pdb; pdb.set_trace()
6 -> print('Name is : ', name)
7    address = get_address(id)
8    return address

As you can notice full function is displayed and with arrow on line 6 telling us the where out debugger is.


Basic command 6 : l (list)
Use 'l' to see the small snippet of code. This command shows 11 lines of code around the current debugging point (5 above, 5 down, and 1 current line).

We can also provide range of line numbers to see specific lines.

->print('Name is : ', name)

(Pdb) l 4,6
4  def print_details(name, id):
5    import pdb; pdb.set_trace()
6 -> print('Name is : ', name)


Basic command 7 : c (continue)
Use 'c' when:

def print_names(name_list):
  import pdb; pdb.set_trace()
  for name in name_list:
    print(name)
  import pdb; pdb.set_trace()
  return None

print_details(['Eddy','Satya','Robin'])

Shell:

> desktop\example.py(1)print_names()
-> for name in name_list:
(Pdb) c
Eddy
Satya
Robin
->return None

Note : The c command printed all elements of name_list and again stopped the execution after for loop where we applied and next breakpoint.


Basic command 8 : q (quit)
Sometimes you find out while debugging that there is some issue in the code and you do not want further code to be executed. Eg. you found out the ID passed in the function is wrong and next lines of code is saving the ID into Database, you definitely do not want this ID to be saved in DB, at this this time you can use q to quit the execution.


Advanced debugging commands


Breakpoints:

Advanced command 1 : b (break)

Syntax :

[([filename:]lineno | function) [, condition]]

filename and condition are optional, we'll see this in example.

Break command is very useful when:

Example 1 (with post-mortem debugging):

def print_names(name_list):
  for name in name_list:
    print(name)
  return None

print_details(['Eddy','Satya','Robin'])

Note: There is no import pdb code in the file.

Run the command:

python -m pdb example.py

Shell:

> desktop\example.py(1)<module>()
-> def print_names(name_list):
(Pdb) b example:3
Breakpoint 1 at desktop\example.py:3

We have added breakpoint in example.py at line number 3. Now we can use c command to continue the execution till line no.3

(Pdb) c
-> print(name)
(PDB)p name
Eddy


Example 2 (adding conditional breakpoints):
Run the command:

python -m pdb example.py

Shell:

> desktop\example.py(1)<module>()
-> def print_names(name_list):
(Pdb) b example:3, name=='Robin'
Breakpoint 1 at desktop\example.py:3
(PDB) c
-> print(name)
(PDB) p name
'Robin'

Our breakpoint skipped first 2 iterations of 'Eddy' and 'Satya' and stopped execution at 'Robin' which 3rd element of the list.
Things to consider:

Advanced command 2 : pp (pretty printer)
pdb also includes of pretty printer. This command is useful especially when working with big dictionaries, json or HTML code. pp beautifies the structure of these data structures in the terminal in a human readable manner.