Appearance
Debugging
The probability of writing a program that runs correctly on the first attempt is very low, typically less than 1%. Bugs of all kinds need to be fixed. Some bugs are simple and can be identified just by looking at the error message, while others are complex, requiring us to know which variable values are correct and which are incorrect when the error occurs. Therefore, a comprehensive set of debugging techniques is necessary to fix bugs.
Print Statements
The first method is simple, direct, and effective: use print()
to output potentially problematic variable values:
python
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
In the output, you can check the printed variable values:
$ 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 it must eventually be removed. Imagine your program filled with print()
statements; the output would contain a lot of junk information. Thus, we have a second method.
Assertions
Wherever print()
is used for inspection, assert
can be used instead:
python
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
def main():
foo('0')
The assert
statement means that the expression n != 0
should evaluate to True
. If it does not, according to the program's logic, subsequent code will definitely fail.
If the assertion fails, the assert
statement itself will raise an AssertionError
:
$ python err.py
Traceback (most recent call last):
...
AssertionError: n is zero!
However, if the program is filled with assert
statements, it is not much better than using print()
. But Python allows you to disable assertions when starting the interpreter with the -O
parameter:
$ python -O err.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Note:
The assertion switch -O
is the uppercase letter "O," not the digit "0."
When disabled, you can treat all assert
statements as pass
.
Logging
Replacing print()
with logging
is the third method. Unlike assert
, logging does not raise errors and can output to files:
python
import logging
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
When you run this, you may find no output except for ZeroDivisionError
. What’s going on?
Don’t worry; just add a configuration line after import logging
and try again:
python
import logging
logging.basicConfig(level=logging.INFO)
You will 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 level of information to record, such as debug, info, warning, and error. When we set level=INFO
, logging.debug
will not take effect. Similarly, setting level=WARNING
will disable both debug
and info
. This way, you can confidently output information at different levels without needing to delete any statements and manage which level of information to output at the end.
Another benefit of logging is that through simple configuration, a single statement can output to multiple places, such as the console and a file.
pdb
The fourth method is to use Python’s debugger, pdb
, which allows you to run the program step-by-step and view the runtime state at any time. Let’s prepare a sample program:
python
# err.py
s = '0'
n = int(s)
print(10 / n)
Now start the debugger:
$ python -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'
Starting with -m pdb
, pdb
points to the next line to be executed: s = '0'
. Enter the command l
to view the code:
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)
Use the command n
to execute the 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 point, you can enter the command p variable_name
to view the variable’s value:
(Pdb) p s
'0'
(Pdb) p n
0
To exit debugging and terminate the program, enter the command q
:
(Pdb) q
This command-line debugging method through pdb
is theoretically powerful, but it can be cumbersome. For example, if you have a thousand lines of code, think about how many commands you need to type to reach line 999. Fortunately, we have another debugging method.
pdb.set_trace()
This method also uses pdb
, but instead of stepwise execution, you simply need to import pdb
and place a pdb.set_trace()
in the area where you suspect an error might occur. This will set a breakpoint:
python
# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # The program will pause here automatically
print(10 / n)
Running this code will pause the program at pdb.set_trace()
and enter the pdb
debugging environment, allowing you to use the command p
to check variables or c
to continue 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 much more efficient than directly starting pdb
for step-by-step debugging.
IDE
If you want a more user-friendly way to set breakpoints and execute code step-by-step, you’ll need an IDE that supports debugging features. Some good Python IDEs include:
- Visual Studio Code: https://code.visualstudio.com/, requires the Python extension.
- PyCharm: http://www.jetbrains.com/pycharm/.
- Eclipse with the PyDev plugin can also debug Python programs.
Summary
Debugging is one of the most painful aspects of programming. Programs often run in unexpected ways, and the statements you expect to execute might not run at all. When this happens, debugging becomes necessary.
While IDEs make debugging more convenient, you’ll ultimately find that logging is the ultimate tool for tracking down issues effectively.