Exception handling and debugging in Python¶
You have probably already seen Python’s exception handling in action from trying to execute invalid syntax, or some other illegal operation. For instance:
>> 1/0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
The first part in the last line says ZeroDivisionError
. This is one of the
Python’s built-in exceptions for arithmetic errors.
Exceptions are raised by different kinds of errors when executing invalid Python code. You may also throw your own errors, and even define custom error types specific to your code base. Please see a more complete list of Python’s built-in exceptions at https://docs.python.org/3/library/exceptions.html.
Below, we shall take a look at some examples which will teach us to how to use exception handling in Python programs. In addition, we are going to study a few debugging strategies to help deal with exceptions that arise when you aren’t expecting them.
Exceptions¶
A few of the more common built in exceptions are listed here:
TypeError
: unsupported operation>>> 1+'apple' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'int' and 'str'
KeyError
: invalid use of key
>>> eng2kor = {'three': 'set', 'two': 'dool', 'one': 'hana'} >>> eng2kor[0] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 0
IndexError
: invalid use of index>>> a = [1, 2, 3] >>> a[4] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range
AttributeError
: attribute reference or assignment failure>>> eng2kor.append('foo') Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'dict' object has no attribute 'append'
SyntaxError
: the most basic type of error when the Python parser is unable to understand a line of code>>> print "I love scientific computing" SyntaxError: Missing parentheses in call to 'print'. Did you mean print("I love scientific computing")?
The above syntax error due to missing parentheses happens mostly when migrating Python2 codes to Python3.
Catching exceptions¶
try/except¶
The try
statement works as follows:
Python attempts to execute the statement(s) between
try
andexcept
If no exception occurs, the
try
block terminates and theexcept
block is ignoredIf an exception does occur during the execution of the
try
block, the rest of the block is skipped, and the exception is passed to theexcept
block.If its type matches the exception named after the
except
keyword, the body of theexcept
clause is executed, and then execution continues after thetry-except
statement.If the type does not match, then the exception will get forwarded to additional
except
blocks if they are present. Otherwise, it gets passed up a layer (potentially all the way to the interpreter). If the interpreter catches the exception then you’ll see one of the above messages: Exceptions.
Now let’s take a look at few examples. In the first example, you will see the error message if you enter an input that is not a number:
1""" 2 3lectureNote/chapters/chapt05/codes/try_except1.py 4 5try-except example, originally from https://docs.python.org/3/tutorial/errors.html 6 7""" 8 9 10while True: 11 try: 12 x = int(input("Please enter an integer: ")) 13 print('The number you entered was = ', x) 14 except ValueError: 15 print("Oops! Could not invert your input to an integer. Try again...")
Exception handlers don’t just handle exceptions when they occur immediately within the try clause, but also when they occur inside functions that are called (even indirectly) in the try clause. For example:
1""" 2 3lectureNote/chapters/chapt05/codes/try_except2.py 4 5try-except example, originally from https://docs.python.org/3/tutorial/errors.html 6 7""" 8 9def this_fails(): 10 print('try-except example') 11 print('1. valid division 1/2') 12 print('2. invalid division 1/0') 13 option = int(input("Please enter an option number -- 1 or 2: ")) 14 15 if option == 1: 16 x = 1./2 17 elif option == 2: 18 x = 1./0 19 return x 20 21try: 22 x = this_fails() 23 print(x) 24except ZeroDivisionError: 25 print('Handling run-time error')
try/finally¶
A finally
block can optionally be used with a try/except
pair, which is intended to
define clean-up actions that must be executed under all circumstances. For instance:
1""" 2 3lectureNote/chapters/chapt05/codes/try_finally.py 4 5try-finally example, originally from https://docs.python.org/3/tutorial/errors.html 6 7""" 8 9def divide(x, y): 10 try: 11 result = x / y 12 print("Result is", result) 13 except ZeroDivisionError: 14 print("Division by zero!") 15 finally: 16 print("Executing finally clause, Thanks for testing me!") 17 18if __name__ == '__main__': 19 print("calling as divide(1.0,2.0)") 20 divide(1.0,2.0) 21 print("calling as divide(1.0,0.0)") 22 divide(1.0,0.0)
Such a clean-up step can be useful for resource management, e.g., closing a file.
Raising exceptions¶
Let’s now take a look at how to raise an exception in your code. In the following
example, we run an iteration using our old example of Newton’s root finding method.
One idea is to raise an exception of ArithmeticError
(see line 33 in the example
below) when the method fails to converge.
1""" 2 3lectureNote/chapters/chapt04/codes/try_except3.py 4 5try-except example, written by Prof. Dongwook Lee for AM 129/209. 6 7Modified by Ian May, FA2021 8 9""" 10 11import numpy as np 12 13def fdf(x): 14 f = x + np.exp(x) + 10/(1 + x**2) - 5 15 df = 1.0 + np.exp(x) - 20*x/(1 + x**2)**2 16 return f,df 17 18def root_finder(func, x0, thresh, MaxIter=100): 19 # Put initial guess in list to store history 20 x = [x0] 21 # Query function at initial guess 22 f,df = func(x0) 23 res = [abs(f)] 24 for i in range(0,MaxIter): 25 # Raise error if slope is very flat 26 if (abs(df)<1.e-5): 27 raise ArithmeticError 28 # Newton's iteration 29 x.append(x[-1] - f/df) 30 # Query function at new guess 31 f,df = func(x[-1]) 32 # Update residual 33 res.append(abs(f)) 34 print(x[-1],f,df) 35 # Check for convergence 36 if (res[-1] < thresh): 37 break 38 39 # If final residual is too big we stagnated 40 if (res[-1] > thresh): 41 raise ArithmeticError 42 43 # Return sequence of guesses and residuals 44 return x,res 45 46 47if __name__ == '__main__': 48 # take an initial value as an input from users 49 x0 = float(input("Please enter an initial search value: ")) 50 # take a threshold value as an input from users 51 threshold = float(input("Please enter a threshold value: ")) 52 # Call Newton's method 53 try: 54 x, res = root_finder(fdf,x0,threshold) 55 except ArithmeticError: 56 print("Newton's method failed for that initial guess") 57 else: 58 print("Newton's method converged in %d iterations" % len(x)) 59 for v,r in zip(x,res): 60 print(v, r)
Try running this code using the initial guesses of \(x=-1/2,0,1,1.1634\).
Python debugging¶
Print statements¶
Print statements can be added almost anywhere in a Python code to print things out to the terminal window as it goes along.
You might want to put some special symbols in debugging statements to flag them as such, which makes it easier to see what output is your debug output and also makes it easier to find them again later to remove from the code, e.g. you might use “+++” or “DEBUG”.
As an example, suppose you are trying to better understand Python scoping and namespaces, as well as the difference between local and global variables. Then this code might be useful:
1"""
2lectureNote/chapters/chapt05/codes/debugging1.py
3
4Originally from
5http://faculty.washington.edu/rjl/classes/am583s2014/notes/python_debugging.html
6
7Debugging demo using print statements
8
9"""
10
11x = 3.5
12y = -22.231
13
14def f(z):
15 global y
16 x = z + 10
17 y = 20.19
18 print("+++ in function f: x = %s, y = %s, z = %s, tt=%s" % (x,y,z,'i love am129/209')) # %s also works for numbers
19 # print("+++ in function f: x = %f, y = %f, z = %f, tt=%s" % (x,y,z,'i love am129/209')) # but in general, %f also works for all numbers
20 return x
21
22print("+++ before calling f: x = %s, y = %s" % (x,y))
23y1 = f(x)
24print("+++ after calling f: x = %s, y = %s, y1= %s" % (x,y,y1))
Note
In the above example, we see two tokens, %s
for strings and
%d
for numbers, as placeholders of what comes after the %
sign, e.g., % (x, y, z)
.
See more examples here. We’ll see more about this when we cover C.
Here the print function in the definition of f(x)
is being used for
debugging purposes. Executing this code gives
$ python3 debugging1.py
+++ before calling f: x = 3.0, y = -22.0
+++ in function f: x = 13.0, y = -22.0, z = 3.0
+++ after calling f: x = 3.0, y = 13.0
If you are printing large amounts you might want to write to a file rather than to the terminal.
The pdb debugger¶
Inserting print statements may work best in some situations (particularly for debugging
logic errors), but it is often better to use a debugger. The Python debugger pdb
is very easy to use, often even easier than inserting print statements and is well worth
learning. See the pdb documentation for
more information. The syntax is very similar to gdb
which we discussed before.
You can insert breakpoints in your code (see line 17 in the below) where control should pass back to the user, at which point you can query the value of any variable, or step through the program line by line from this point on:
1"""
2lectureNote/chapters/chapt05/codes/debugging2.py
3
4Originally from
5http://faculty.washington.edu/rjl/classes/am583s2014/notes/python_debugging.html
6
7Debugging demo using print statements
8
9Debugging demo using pdb.
10"""
11
12import pdb
13
14x = 3.
15y = -22.
16
17def f(z):
18 x = z+10
19 pdb.set_trace()
20 return x
21
22y = f(x)
23
24print("x = ",x)
25print("y = ",y)
Of course one could set multiple breakpoints with other pdb.set_trace()
commands. For
the above example we might do this as below. Upon running the above example, we get the
prompt for the pdb
shell when we hit the breakpoint
$ python3 debugging2.py
> /home/ian/UCSC/AM129Lectures/Python/debugging2.py(18)f()
-> return x
(Pdb) p x
13.0
(Pdb) p y
-22.0
(Pdb) p z
3.0
Note that p
is short for print
which is the same as gdb
command. You could
also type print(x)
but this would then execute the Python print command instead of
the debugger command (though in this case it would print the same thing).
There are many other pdb
commands, such as next
to execute the next line, continue
to continue executing until the next breakpoint, etc.
$ python3 debugging2.py
> /home/ian/UCSC/AM129Lectures/Python/debugging2.py(18)f()
-> return x
(Pdb) p z
3.0
(Pdb) continue
x = 3.0
y = 13.0
(See the pdb documentation for more details.)
Debugging after an exception occurs¶
Often code has bugs that cause an exception to be raised, causing the program to halt execution. Consider the following example:
1"""
2
3lectureNote/chapters/chapt05/codes/pdb_example.py
4
5"""
6
7def division_by_zero(x):
8 x /= x-1 # equiv. to x = x/(x-1)
9 return x
10
11# import pdb
12for i in range(5,0,-1):
13 # pdb.set_trace()
14 soln = division_by_zero(float(i))
In IPython you can run this by calling
>>> %run pdb_example.py
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
~/UCSC/AM129/LectureF21/Python/Fundamentals/pdb_example.py in <module>
12 for i in range(5,0,-1):
13 # pdb.set_trace()
---> 14 soln = division_by_zero(float(i))
~/UCSC/AM129/LectureF21/Python/Fundamentals/pdb_example.py in division_by_zero(x)
6
7 def division_by_zero(x):
----> 8 x/=x-1 # equiv. to x=x/x-1
9 return x
10
ZeroDivisionError: float division by zero
At some point there must be a division by zero. To figure out when this happens, we
could insert a pdb.set_trace()
command in the loop and step through it until the
error occurs and then look at i
. Alternatively, and more easily, we can use a
post-mortem analysis after it dies via pdb.pm()
>>> import pdb
>>> pdb.pm()
> <string>(8)division_by_zero()
(Pdb) p i
1
(Pdb) p soln
2.0
(Pdb) p x
1.0
This starts up pdb
at exactly the point where the exception is about to occur. We
see that the divide by zero happens when i = 1
. Compare this to our usage of bt
in gdb
.