The probability of writing a program that works correctly on the first try is extremely low—essentially less than 1%. There will always be various bugs that need fixing. Some bugs are simple to identify by just looking at the error message; others are complex, requiring us to verify which variables hold correct values and which hold incorrect ones when the error occurs. Therefore, a comprehensive set of debugging techniques is necessary to fix bugs.
The first method is simple, straightforward, crude, and effective—use print() to output the values of potentially problematic variables for inspection:
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
After execution, check the printed variable values in the output:
$ python err.py
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
The biggest drawback of using print() is that you have to remove these statements later. Imagine a program cluttered with print() calls—its output will be filled with irrelevant noise. For this reason, we have a second method.
Anywhere you use print() for debugging can be replaced with an assertion (assert):
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
def main():
foo('0')
An assert statement means the expression n != 0must be True; otherwise, the subsequent code will inevitably fail based on the program’s logic.
If the assertion fails, the assert statement itself raises an AssertionError:
$ python err.py
Traceback (most recent call last):
...
AssertionError: n is zero!
A program filled with assert statements is barely better than one with print() calls. However, you can disable assertions by running the Python interpreter with the -O flag:
$ python -O err.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Note
The assertion flag
-Ois the uppercase English letter O, not the number 0.When disabled, all
assertstatements are treated aspass.
The third method is to replace print() with the logging module. Unlike assert, logging does not raise errors and can write output to files:
import logging
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
The logging.info() function outputs a message. When you run the code, you’ll see only the ZeroDivisionError—no logging output. Why?
Don’t worry—add a configuration line right after import logging and try again:
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
Now you’ll see the output:
$ python err.py
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print(10 / n)
ZeroDivisionError: division by zero
This is the advantage of logging: it allows you to specify the logging level (e.g., debug, info, warning, error). When you set level=INFO, logging.debug() messages are ignored. Similarly, setting level=WARNING ignores debug and info messages. This lets you safely output messages of different levels without deleting them—you can later control which levels are displayed globally.
Another benefit of logging is that with simple configuration, a single statement can output to multiple destinations (e.g., the console and a file).
The fourth method is to use Python’s built-in debugger pdb to run the program step-by-step and inspect its state at any time. First, prepare the program:
# err.py
s = '0'
n = int(s)
print(10 / n)
Start the debugger:
$ python -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'
When launched with -m pdb, pdb positions itself at the next line of code to execute (-> s = '0'). Enter the command l (list) to view the code:
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)
Enter n (next) to execute code step-by-step:
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
-> print(10 / n)
At any time, enter p <variable> to inspect a variable:
(Pdb) p s
'0'
(Pdb) p n
0
Enter q (quit) to end debugging and exit the program:
(Pdb) q
In theory, debugging via pdb in the command line is universal—but it’s extremely cumbersome. Imagine stepping through 1,000 lines of code to reach line 999! Fortunately, we have another debugging method.
This method also uses pdb but avoids step-by-step execution from the start. Simply import pdb and place pdb.set_trace() at the potential error site to set a breakpoint:
# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # Program pauses here automatically
print(10 / n)
Run the code—the program will pause at pdb.set_trace() and enter the pdb debugging environment. Use p to inspect variables or c (continue) to resume execution:
$ python err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
File "err.py", line 7, in <module>
print(10 / n)
ZeroDivisionError: division by zero
This method is far more efficient than launching pdb for full step-by-step debugging—but still not ideal.
For a smoother debugging experience (e.g., easy breakpoint setting and step-by-step execution), use an IDE with debugging support. Popular Python IDEs include:
Debugging is the most frustrating part of programming. Programs often execute in unexpected flows, and statements you expect to run may not execute at all—this is when debugging becomes essential.
While IDEs make debugging convenient, you’ll eventually find that logging is the ultimate debugging tool.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def foo(s):
n = int(s)
assert n != 0, "n is zero!"
return 10 / n
def main():
foo("0")
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
logging.basicConfig(level=logging.INFO)
s = "0"
n = int(s)
logging.info("n = %d" % n)
print(10 / n)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pdb
s = "0"
n = int(s)
pdb.set_trace()
print(10 / n)