Back to Article
PE100-06: Exceptions
Download Notebook

PE100-06: Exceptions

Most of the time, the code we write does exactly what we expect. Our numbers are added up, files are written and read, and users type their input in neat little boxes. Sometimes, though, something goes wrong. Maybe the disk storage space filled up, or we try to write to a file in a directory we don’t have access to (or maybe the directory doesn’t even exist). When things like this happen, the Python interpreter stops the normal flow of execution.

Take a look at an exception:

In [1]:
funny_number = 1/0
ZeroDivisionError: division by zero

When you run the above, Python will notice the error, stop the code from running, and point out that a “ZeroDivisionError” has occurred. Since this kind of thing wasn’t supposed to happen (division by zero is considered a Bad Thing(tm) by most people) we can say the situation we’re faced with is an exception. And indeed, Python’s error handling mechanisms are based on what are called “exceptions”.

When Python saw the “division by zero” error, it stopped running the rest of the code. It created one of these Exceptions, and then it threw it. Nothing in our one-line example tried to do anything about that exception, so Python just let the program crash and it printed the helpful error messages for us.

Most of the time, we want our code to be able to handle exceptions when they arrise. We want something that can catch these exceptions when they’re thrown. For that, we need to use Python’s try statement.

Try, try again


try is how we safely wrap up a bit of code so that if something in there fails and an exception is thrown, we have a way to catch it. For example:

In [5]:
try:
    denominator = int(input('Please enter the denominator'))
    funny_number = 1000/denominator
    print('the result was', funny_number)
except ZeroDivisionError:
    print('Looks like someone tried to divide by zero.')
print("Either we were able to do the division or else we successfully handled an exception.")

    
Please enter the denominator 0
Looks like someone tried to divide by zero.
Either we were able to do the division or else we successfully handled an exception.

Try running the code above a few times. In the input area, try some different numbers each time. Maybe 4, 0, and -2. Notice that division by non-zero numbers works as expected. Notice also that division by zero now lets us print out an error message instead of crashing. Once we’re done handling the exception, the program resumes with the first line after the try/except structure.

In fact, there might be several except clauses if there are several kinds of exceptions that might be thrown. For example, let’s figure out how to share a pizza.

In [8]:
try:
    people = int(input('How many people:'))
    slices = 8/people
    print('Each gets', format(slices, '.2f'), 'pieces.')
except ValueError:
    print("The number of people must be a valid integer.")
except ZeroDivisionError:
    print("Seriously? There are zero people sharing a pizza?")

print("Whatever happened up there, this is the first line of code after")
print("the try/except structure.")
How many people: 0
Seriously? There are zero people sharing a pizza?
Whatever happened up there, this is the first line of code after
the try/except structure.

As you try different numbers of people, you can see that division by zero is, of course, handled. You can also enter things that aren’t integers. In response to the prompt, you could enter “Fred”. That can’t be converted to an integer, so the int() function throws an error. The except ValueError clause catches that exception and prints out a message.

Notice that after either exception handler executes its code, the flow of control goes down to the next line after the try/except structure. In this case, that line is one that prints out a message saying it’s the first line of code after the try and all of the excepts.

Sometimes it’s hard to predict what exception might be thrown in a section of code. In that case, we can use just except: without any exception type. This serves as a “catch-all” handler.

In [10]:
try:
    my_file = open('/tmp/ThisFileIsUnlikelyToExist', 'r')
    people = int(input('How many people:'))
    slices = 8/people
    print('Each gets', format(slices, '.2f'), 'pieces.')
except ValueError:
    print("The number of people must be a valid integer.")
except ZeroDivisionError:
    print("Seriously? There are zero people sharing a pizza?")
except:
    print("The catch-all handler has been awoken from its slumber.")
    print("I don't know what went wrong, except I can tell you it")
    print("wasn't a ValueError or a ZeroDivisionError, because")
    print("those would have been caught by more specific handlers")
    print("further up the list.")
The catch-all handler has been awoken from its slumber.
I don't know what went wrong, except I can tell you it
wasn't a ValueError or a ZeroDivisionError, because
those would have been caught by more specific handlers
further up the list.

Indeed, if we’re lazy (or in a hurry) then we can get by with just a plain except clause and let the user figure it out later:

In [11]:
try:
    my_file = open('/tmp/ThisFileIsUnlikelyToExist', 'r')
    people = int(input('How many people:'))
    slices = 8/people
    print('Each gets', format(slices, '.2f'), 'pieces.')
except:
    print("There was some sort of problem. I have no idea what.")
There was some sort of problem. I have no idea what.

Using just a plain catch-all exception handler doesn’t give you much to work with, but it is slightly better than nothing. Your code won’t crash outright but you won’t much information about what went wrong. If only there was a way to examine that exception, to peer in and divine its secret nature…

Yep. Here you go…

In [12]:
try:
    my_file = open('/tmp/ThisFileIsUnlikelyToExist', 'r')
    people = int(input('How many people:'))
    slices = 8/people
    print('Each gets', format(slices, '.2f'), 'pieces.')
except Exception as err:
    print("Error:", err)
Error: [Errno 2] No such file or directory: '/tmp/ThisFileIsUnlikelyToExist'

What we’ve done is catch any kind of exception (except Exception) and assigned it to a variable named “err”. Then we can print out err. We could even convert err to a string and search for the interesting parts (like the filename of our missing file) and do some clever error handling based on what specifically went wrong.

Python has a few more tricks when it comes to exception handling, and these can be handy for making your code more readable.

Fancy exception handling


A try/except structure can have an else clause. This clause will only be executed if no exception was thrown.

In [16]:
try:
    people = int(input('How many people:'))
    slices = 8/people
except Exception as err:
    print("Error:", err)
else:
    print('Each gets', format(slices, '.2f'), 'pieces.')
How many people: 0
Error: division by zero

If the user enters something that can be converted to an integer and is non-zero, then the program continues, finishing up the try block and executing the else block. On the other hand, if an exception of any type is thrown then the “number of pieces” message will never be printed.

There is also a finally clause. This one will run after everything else has happened, no matter what.

In [17]:
try:
    output_file = open("/tmp/output", "w")
    people = int(input('How many people:'))
    slices = 8/people
except Exception as err:
    print("Error:", err)
else:
    print('Each gets', format(slices, '.2f'), 'pieces.')
finally:
    output_file.close()
How many people: 0
Error: division by zero

In the try clause, a file opening was added. In the finally clause, the file will be closed whether an exception was thrown or not.

How useful are else and finally clauses? It’s true they’re not absolutely necessary. Most programming languages don’t have anything like that. You can always juggle your code around and get by with just try and except. On the other hand, these two clauses can make your code easier to read and understand. Your precise intention can be discerned.

We’ve seen how to write Python code that catches errors without crashing. This technique works in both regular Python programs and in Jupyter Notebooks. Next up, we’ll turn back to ways of storing information. This time we’ll look at lists.