As we have seen, sometimes programs fail to produce a result, ending instead in an error. Sometimes, we do not even get that far – Python rejects our program when we type it in, before we have a chance to run it. Sometimes the error is in our program itself, the programmer’s fault. Sometimes it is a problem with unexpected input from the user, or the absence of an expected file.
In this chapter, we look at strategies for detecting, coping with, and recovering from these various types of error.
We will begin by looking at Python’s mechanism for dealing with null results. You might have noticed that if we forget the return
keyword, we see this:
Python
>>> def f(a, b): a + b
...
>>> f(1, 2)
>>>
It looks as if nothing is returned. In fact, the result is a special value called None
:
Python
>>> f(1, 2) is None
True
>>> None
>>>
(We use the is
operator here instead of ==
, for reasons beyond the scope of this book.) Note that None
has no printed representation here unless we explicitly use print
, or if it appears in a compound structure:
Python
>>> print(f(1, 2))
None
>>> def g(a):
... if a > 0:
... return a
... else:
... pass
...
>>> list(map(g, [-1, 0, 1, 2, 3]))
[None, None, 1, 2, 3]
The None
value has a type. In fact, it is the only value of that type:
Python
>>> type(None)
<class 'NoneType'>
Some operations which raise errors have equivalent versions which instead return None
on an error. For example, looking up a key in a dictionary with the get
method instead of with ordinary indexing returns None
:
Python
>>> d = {1: 'one', 2: 'two', 3: 'three'}
>>> d[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 0
>>> d.get(0)
>>> v = d.get(0)
>>> print(v)
None
So, we could write a function to look up a list of given keys in a dictionary, returning a list of only the values for which lookup succeeds, and ignoring those for which it fails:
(We can write is not
as well as is
). For example:
Python
>>> found_values([1, 2, 3], {1: 'one', 2: 'two'})
['one', 'two']
Python has a mechanism for representing, detecting, and responding to exceptional situations. That mechanism is known as an exception. We have just seen an example:
Python
>>> d = {1: 'one', 2: 'two', 3: 'three'}
>>> d[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 0
The exception here is KeyError
, and it carries along with it the number 0
so we know which key could not be found. Let us write a dictionary lookup function which prints our own message and returns -1
if the lookup fails:
There are two new words here: try
and except
. The statements after try
will be attempted. If they succeed, the function returns as normal. If they fail with KeyError
, control transfers to the except
section. Here is an example failing call:
Python
>>> safe_lookup({1: 'one', 2: 'two', 3: 'three'}, 0)
Could not find value for key 0
-1
By this exception mechanism, we can handle exceptional circumstances without stopping the program, or terminate the program early, but in a controlled manner.
Here are some of Python’s standard exceptions:
Here is an example of the NameError
exception:
Python
>>> def add3(x, y): return x + y + z
...
>>> add3(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in add3
NameError: name 'z' is not defined
>>> z = 10
>>> add3(1, 2)
13
Notice that z
does not have to be defined after the definition of add3
– it may be supplied afterward. This is rather bad practice through, of course.
As well as handling the standard exceptions, we can raise them ourself with the raise
construct. Here is a function to build a list of repeated elements:
This function raises ValueError
if asked to create a list of negative length. For example:
Python
>>> repeated(1, 10)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
>>> repeated(1, -10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in repeated
ValueError
We can give a name to an exception as we catch it, allowing us to use raise
to re-raise the exception:
Here, we have decided that a bad key is a fatal error, but we wish to provide some debugging information including the key and dictionary before the program ends.
We can catch any exception by using except
Exception
:
We would only normally do this to gather up any un-handled exceptions in a large program, and report them cleanly before exiting. Otherwise it is always best to specify which exception we expect to have to handle.
We should try to keep the part between try
and except
as small as possible, so it is clear which statement or statements might fail to complete. We can do this using an optional else
part:
Notice that the variable result
is available in the else
portion. We can now rewrite, using exceptions, our guessing_game
function from the questions to chapter 3. We wish to properly deal with the possibility that the input from the user is either not a number, resulting in a ValueError
exception, or that the number is not in range. We will encapsulate this in the new get_guess
function, using a fairly benign form of recursion:
We minimise the portion between try
and except
by including an else
section. Now the error handling is confined to the get_guess
function, and the main function is relatively simple:
Here is the full program:
Being a language which evolved slowly, with no grand design, there is in Python little consistency. Some functions signal error by returning -1
, some by returning None
, some by raising exceptions. It is important to check the documentation, and make sure to include error handling in our programs at all appropriate points. Even if we have to exit the program on a particularly unusual error (e.g disk full), we can at least print a message. Taking care in this circumstances is crucial to building reliable programs, especially larger ones.
We have finally addressed the problem of how to deal with errors which occur when running our programs: to detect them, handle them, and recover from them. We have learned about the null result None
and how to take advantage of it. We can now add exceptions to our toolbox, choosing between error avoidance and error detection as appropriate in each situation.
In the next chapter, we return to the topic of file processing, writing some more complete programs.
Write a function which, given a list of strings, such as [’1’, ’10’, ’ten’, ’tree’]
returns their sum, ignoring anything which is not a number made of digits.
Rewrite your solution using map
, filter
, and sum
, if you did not use them originally.
Use exceptions to write a safe_division
function which returns 0
if asked to divide by zero.
Use exceptions to write a function to prune a dictionary: dict_take(a, b)
should yield a new dictionary with keys and values drawn from dictionary b
, but only if the key exists in dictionary a
.
Write a function safe_union
which builds the union of two dictionaries, but raises KeyError
if there is a clash of keys.
Write a function add_exception
to add value to a set, but which raises KeyError
if the value already exists in the set.