Debugging tools are at the heart of any programming language.
And as a developer, it's hard to make progress and write clean code unless you know your way around these tools.
This article will help you get acquainted with one such tool: The Python Debugger (pdb)
Note that this is a debugging tutorial. I assume that you are familiar with at least one programming language, and have an idea about writing test cases.
How to Get Started with pdb
There are two ways to invoke pdb
:
1. Call pdb
externally
To call pdb
when on a terminal you can call it while executing your .py
file.
python -m pdb <test-file-name>.py
If you use poetry and pytest you can call pdb
using --pdb
flag in the end.
poetry run python <path/to_your/test_file.py> --pdb
To call pdb
with Docker, poetry
, and pytest
you can use the following syntax:
COMPOSE_PROJECT_NAME=<test_docker_image_name> docker-compose run --rm workers poetry run pytest <path/to_your/test_file.py>::<name_of_the_test_function> --pdb
You will always add the --pdb
flag after the name of your test file. This will open the pdb
console when the test breaks. But remember --pdb
is a pytest
flag.
2. Add a breakpoint with pdb
There can be cases when you get a false positive(s) in a test. Your test case might pass but you don't get the data you were expecting.
What if you want to read the raw database query? In that case you can call pdb
from inside the Python function.
To break into the pdb
debugger, you need to call import pdb; pdb.set_trace()
inside your function.
Let's understand this with a nested function example:
# file1.py
from . import function3
def function1():
// logic for function1
function3()
# file2.py
def function2():
// logic for function2
// some database query
# file3.py
from . import function2
def function3():
// logic for function3
function2()
// logic for function3 continues
In the above code, one function calls another.
You want to add a breakpoint in function2
to understand what is actually happening in the function.
You can add a breakpoint with the following statement:
import pdb; pdb.set_trace()
# file2.py
1. def function2():
2. // logic for function2
3. // line 1
4. // line 2
5.
6. import pdb; pdb.set_trace();
7.
8. // some database query
9. // line 3
10. // line 4
pdb
opens its console when your code breaks. Something like this:
(Pdb)
When the Python interpreter executes line2
, it will read the breakpoint and open the pdb
console. We use pdb
commands to navigate the code. We will learn these commands in the next section.
Common pdb
Commands
pdb
is an interactive command line debugger. You can't harness its full potential unless you are familiar with its commands.
Like every other console log, pdb
will tell you exactly at which line your code breaks.
The print command
Let's say you have a test case with an assert
statement. Something like this:
# test.py
def test1():
...
result = function1()
assert result.json = {'status_code':1, 'status': 'saved', 'description':'data saved'}
You will use the p
command to print a value to the console.
(Pdb) p result.json
{'status_code':1, 'status': 'saved', 'description':'data saved'}
This prints the value the variable holds.
The up command
The up command moves you one frame up the stack.
In case of nested function calls, it will move you up into the function that called your function.
Let's take an example:
# test.py
def function1():
print("invoking function1")
import pdb;pdb.set_trace()
print("function1 invoked")
def function2():
print("invoking function2")
function1()
print("function2 invoked")
def function3():
print("inside function3")
function2()
print("function3 invoked")
# starting the call with function2()
function3()
In pdb
we will call it this way:
$ python -m pdb test.py
> test.py(1)<module>()
-> def function1():
(Pdb) n
> test.py(7)<module>()
-> def function2():
(Pdb) n
> test.py(13)<module>()
-> def function3():
(Pdb) n
> test.py(20)<module>()
-> function3()
(Pdb) n
inside function3
invoking function2
invoking function1
> test.py(4)function1()
-> print("function1 invoked")
(Pdb) n
function1 invoked
--Return--
> test.py(4)function1()->None
-> print("function1 invoked")
(Pdb) u
> test.py(9)function2()
-> function1()
(Pdb) l
4 print("function1 invoked")
5
6
7 def function2():
8 print("invoking function2")
9 -> function1()
10 print("function2 invoked")
11
12
13 def function3():
14 print("inside function3")
(Pdb) u
> test.py(15)function3()
-> function2()
(Pdb) l
10 print("function2 invoked")
11
12
13 def function3():
14 print("inside function3")
15 -> function2()
16 print("function3 invoked")
17
18 # starting the call with function2()
19
20 function3()
(Pdb) u
> test.py(20)<module>()
-> function3()
(Pdb) l
15 function2()
16 print("function3 invoked")
17
18 # starting the call with function2()
19
20 -> function3()
[EOF]
(Pdb) u
> <string>(1)<module>()
Here we start with invoking function3()
. The execution stops when it encounters import pdb
.
pdb
opens the console and waits for the input. We type u
for up, and it returns the calling function: function2()
. On the next u
command it returns function3
(the function that calls function2
).
We are using the l
command. It is the list command. It lists exactly where the current execution line is.
The step command
To understand the step
command, let's continue with the previous example.
# test.py
1. def test1():
2. ...
3. result = function1()
4. assert result.json.status_code == 1
5. assert result.json.status == 'saved'
6. assert result.json.description == 'data saved'
7.
# function_file.py
def function1():
foo = ['bar']
...
You suspect that the result returned from function1()
is incorrect. Your code beaks at line 6. How do you go step into line 3?
You will use the up
command first and finally step into the function with the s
command.
(Pdb) u
assert result.json.status == 'saved'
(Pdb) u
assert result.json.status_code == 1
(Pdb) p assert result.json.status_code
1
(Pdb) u
result = function1()
(Pdb) s
(Pdb) n
foo = ['bar']
When you step into function1()
, the pdb
console will start to print statements from that function.
Conclusion
pdb
is a powerful debugger. This tutorial intends to get you familiar with pdb
basics.
I recommend reading its documentation to explore the full potential.