Back to Contents

8. When Things Go Wrong

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.

When there is no result

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:

image

(We can write is not as well as is). For example:

Python
>>> found_values([1, 2, 3], {1: 'one', 2: 'two'})
['one', 'two']

Exceptions

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:

image

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.

Standard exceptions

Here are some of Python’s standard exceptions:

image

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.

Raising exceptions ourselves

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:

image

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:

image

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.

Catching any exception

We can catch any exception by using except Exception:

image

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.

Keeping exceptions small

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:

image

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:

image

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:

image

Here is the full program:

image

Common problems

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.

Summary

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.

Questions

  1. 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.

  2. Rewrite your solution using map, filter, and sum, if you did not use them originally.

  3. Use exceptions to write a safe_division function which returns 0 if asked to divide by zero.

  4. 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.

  5. Write a function safe_union which builds the union of two dictionaries, but raises KeyError if there is a clash of keys.

  6. Write a function add_exception to add value to a set, but which raises KeyError if the value already exists in the set.