13. Exceptions
Coding is an error-prone activity. It is difficult to anticipate when something might go wrong, and even if we could anticipate problems, it is equally, if not more, difficult to decide what to do when something does go wrong. In Python, we can use try-except-finally
or just try-except
to handle problems that might occur. Generally speaking, there are two types of problems that will bring your program to a grinding halt: errors
and exceptions
. Errors cannot be handled, but exceptions can be handled (handle by our code). Let’s look at some examples of how to use try-except
to handle problems.
13.1. Divide by zero
In math, any (positive) number divided by zero results in infinity. In Python, if you attempt to divide a number by zero, an exception
will be thrown. An exception is just a very specific signal indicating that something has gone wrong. The particular exception thrown when attempting to divide a number by zero will be a ZeroDivisionError
type of exception. Once an exception is thrown, your code will crash altogether at that very point.
110 / 0 # ZeroDivisionError thrown
2print('hello, world') # we will never reach this statement
You can use try-except
to attempt to catch
the exception and handle it as appropriate. Below, we simply print an informative message that the denominator is zero.
1# throws ZeroDivisionError
2a = 20
3b = 0
4c = a / b
5
6# try/catch ZeroDivisionError
7try:
8 a = 20
9 b = 0
10 c = a / b
11except ZeroDivisionError:
12 print('your denominator is zero')
If we wanted to capture the details of the exception, we can use the as
keyword to alias the exception as a variable.
1try:
2 a = 20
3 b = 0
4 c = a / b
5except ZeroDivisionError as zde:
6 print(f'error: {zde}')
Many times, we just want a quick and dirty way to handle exceptions or we do not really know what types of exceptions will be thrown. In these cases, we can omit the exception type.
1try:
2 a = 20
3 b = 0
4 c = a / b
5except:
6 print(f'something went wrong')
The finally
block is always guaranteed to run in a try-catch
statement. The catch
block is only executed if the particular exception we are trying to catch is thrown. Only one catch
block will ever be executed since as soon as an exception is thrown, your program will break (exceptions are thrown one at a time). In the example below, we attempt to divide a by b. We initialize c to None
. If there is a division by zero exception, we simply log it (print out the fact that such thing has happened). Since c must always have a valid integer value, the finally
block checks to see if c is set, and if not, sets it to -1.
1a = 20
2b = 0
3c = None
4
5try:
6 c = a / b
7except:
8 print(f'something went wrong')
9finally:
10 c = -1 if c is None else c
13.2. Accessing invalid index
An IndexError
will be thrown when we attempt to access an element by an index that is outside the bounds of the list indices.
1# throws IndexError
2names = ['John', 'Jack', 'Joe']
3print(names[4])
4
5# try/catch IndexError
6names = ['John', 'Jack', 'Joe']
7
8try:
9 print(names[4])
10except IndexError:
11 print('no such index')
13.3. Accessing invalid key
A KeyError
will be thrown when we attempt to access a value with a key that does not exists in a dictionary.
1# throws KeyError
2
3students = {'John': 18, 'Jack': 19}
4
5print(students['Joe'])
6
7# try/catch KeyError
8
9students = {'John': 18, 'Jack': 19}
10
11try:
12 print(students['Joe'])
13except KeyError:
14 print('you tried to access an entry that does not exists')
13.4. Accessing invalid key
We can catch multiple types of exceptions. In the example below, we have a dictionary where the keys are car makes and the associated values are models. If we attempt to use a key that does not exists in the dictionary, a KeyError
will be thrown, and if we attempt to use an index that is out of bounds for the list, an IndexError
will be thrown.
1def get_best_car(make, model):
2 makes = {
3 'honda': ['accord', 'civic', 'crv'],
4 'toyota': ['camry', 'avalon', 'sienna']
5 };
6
7 return makes[make.lower()][model]
8
9
10# valid
11print(get_best_car('Honda', 1))
12
13# throws IndexError
14print(get_best_car('Honda', 4))
15
16# throws KeyError
17print(get_best_car('Ford', 1))
18
19# catch multiple exceptions
20try:
21 print(get_best_car('Ford', 3))
22except IndexError:
23 print('invalid model')
24except KeyError:
25 print('invalid make')
13.5. Raising an exception
We can also throw or raise an exception.
1def convert_grade(letter):
2 grades = {
3 'a': 4.0,
4 'b': 3.0,
5 'c': 2.0,
6 'd': 1.0,
7 'f': 0.0
8 }
9
10 grade = letter.lower()
11 if grade in grades:
12 return grades[grade]
13 else:
14 raise KeyError(f'{letter} is invalid')
15
16
17convert_grade('A')
18convert_grade('B')
19convert_grade('C')
20convert_grade('D')
21convert_grade('F')
22convert_grade('G')
ValueError
is the appropriate exception type to throw when an argument has the right type but wrong value. In the example below, we have a method to compute the area of a square. Note that the side of a square must always be positive. We need to check to see if the side passed into the method is valid; if it’s not valid, then an exception should be thrown.
1def get_area(side):
2 if side < 1:
3 raise ValueError(f'{side} is not greater than zero.')
4 return side * side
5
6print(get_area(10))
7print(get_area(-10))
13.5.1. Exercise
John and Jack took some tests. Jack missed the first test and so there was no score reported for that test. A function to compute the average score of a list of scores was written as below. Modify this code to handle the exception as a result of summing over a list of numbers (scores) with possibly invalid data types.
1def get_average(grades):
2 total = sum(grades)
3 n = len(grades)
4 average = total / n
5 return average
6
7john = [88, 90, 85, 100]
8jack = [None, 88, 100, 100]
9
10print(get_average(john))
11print(get_average(jack))
Solution.
1def get_average(grades):
2 try:
3 total = sum(grades)
4 n = len(grades)
5 average = total / n
6 return average
7 except TypeError:
8 valid_grades = [g for g in grades if isinstance(g, int)]
9 return get_average(valid_grades)
10
11john = [88, 90, 85, 100]
12jack = [None, 88, 100, 100]
13
14print(get_average(john))
15print(get_average(jack))
13.6. User-defined exceptions
We can define our own exceptions by extending the Exception
class. All built-in Python exceptions inherits from Exception
, however, they all have the word Error
as a part of their names (e.g. IndexError
, KeyError
, ValueError
, etc.). This naming convention is a bit confusing as errors cannot be handled (but exceptions can) and if the root class is Exception
why not have that word as a part of the name instead of Error
(e.g. IndexException
).
1class NegativeValueError(Exception):
2 def __init__(self, value):
3 self.value = value
4 super().__init__(f'{value} is negative.')
5
6def get_area(side):
7 if side < 1:
8 raise NegativeValueError(side)
9 return side * side
10
11print(get_area(10))
12print(get_area(-10))
13.6.1. Exercise
Write a function to generate random dimensions for a triangle, square and rectangle. The dimensions (e.g. base, height, width, length) should be in the range [1, 10]. The function should return a dictionary that looks like the following.
{'shape': 'triangle', 'dimensions': [4, 10]}
{'shape': 'square', 'dimensions': [2]}
{'shape': 'rectangle', 'dimensions': [8, 7]}
If a user requests a random circle, throw an InvalidShapeError
(you have to create a new exception type). Invoke this new function passing in randomly a request for triangle, square, rectangle and circle.
Solution.
1from random import randint, choice
2
3class InvalidShapeError(Exception):
4 def __init__(self, shape):
5 self.shape = shape
6 super().__init__(f'{shape} is invalid.')
7
8def get_shape(shape):
9 shapes = {
10 'triangle': 2,
11 'square': 1,
12 'rectangle': 2
13 }
14
15 if shape not in shapes:
16 raise InvalidShapeError(shape)
17
18 return {
19 'shape': shape,
20 'dimensions': [randint(1, 10) for _ in range(shapes[shape])]
21 }
22
23all_shapes = ['triangle', 'square', 'rectangle', 'circle']
24
25for _ in range(10):
26 try:
27 shape = get_shape(choice(all_shapes))
28 print(shape)
29 except InvalidShapeError as ise:
30 print(f'Problem: {ise}')