Overview

In this reading we’ll introduce Python’s exception handling facilities, which is the primary way to deal with error situations in Python code.

Topics

  • Exception handling

  • How to recover from errors

  • The try/except statement

  • Exception objects

  • Raising and catching exceptions

  • Catch-all exception handlers

  • Multiple exception handlers

Disclaimer

This reading and the next are not "fun" readings. We won’t see cool new ways to accomplish neat stuff in a few lines of code. Instead, we will do two things:

  • learn how to handle errors correctly

  • understand better how Python works internally

Exception handling is a surprisingly tricky subject that often causes a lot of confusion for new programmers (and experienced ones!). The syntax is easy to learn, but understanding how to use exceptions properly is a lot harder. Some interesting design decisions will come up!

Error handling strategies

Many possible errors can occur while running a Python program:

  • division by zero

  • accessing a non-existent index of a list

  • accessing a non-existent key in a dictionary

  • trying to store a new value into a tuple

  • trying to open a file (for reading) that does not exist

  • etc.

How should errors be handled? Consider:

>>> a = 1 / 0

This will not give a legal value (Python doesn’t have infinitely large integers ). So how should this error be handled? There are many possibilities.

Silent failure

We could ignore the error and just continue execution. This is called "silent failure", and as you might guess, it’s not a very good idea. Ignoring an error might cause the program to crash, or do something even worse (like allowing a malicious user to break into a system). Silent failure is (unfortunately) common in some languages, notably C and Javascript.

Another problem with silent failure is that the user is given no indication that an error even occurred! Therefore, by definition they also don’t know why the error occurred or where in the code it occurred. (They need that information in order to fix the code.)

Halt the program

When an error occurs, we could simply halt the program. The advantage of this is that it prevents a crash later on. But there are still problems.

First of all, it’s too drastic. Some errors can be recovered from. For instance, if you try to read from a nonexistent file, if the error could somehow be detected, you could inform the user that the file wasn’t found and ask them what to do. Options might include "exit now", or "ask for another file".

Second, we don’t know why the error occurred or where it occurred, only that it occurred. If there is a bug, this is not enough information to allow us to fix the bug.

Print an error message and halt the program

We could print an informative error message before halting the program.

Advantages:

  • It prevents a crash.

  • We know why the error happened.

Problems:

  • It’s too drastic (some errors can be recovered from!)

  • We still don’t know where the error occurred.

Print an error message and a traceback and halt the program

We could print an informative error message stating why and where the error occurred and halt the program.

Advantages:

  • It prevents a crash.

  • We know why and where the error happened.

Problems:

  • It’s too drastic (some errors can be recovered from!)

This is what Python does by default. Let’s create an error situation in Python:

1>>> a = 1 / 0
2Traceback (most recent call last):
3  File "<stdin>", line 1, in <module>
4ZeroDivisionError: division by zero

There are two components to this error message:

  • an error message (line 4) states what kind of error occurred and more specifically why the error occurred in this particular case;

  • a traceback (lines 2-3) states where the error occurred. [1]

We’ll look into tracebacks in greater depth in the next reading.

Error recovery

Errors do not have to result in the termination of the entire program! Many kinds of errors can be recovered from. For instance:

  • bad input → prompt for new input

  • nonexistent files → use a different file

  • etc.

There is no general rule on how best to recover from errors; it’s just too specific to the particular situation where the error occurs. Instead, we need a general mechanism that can allow us to adopt whatever error-handling strategy might make the most sense in the context in which the error occurs. Exception handling is that general mechanism.

Code written using exception handling essentially says this:

  • Under normal circumstances, execute a chunk of code A.

  • If an error EA happens while executing code chunk A, abandon code chunk A and instead execute a chunk of code EA'.

  • Otherwise, if an error EB happens while executing code chunk B, abandon code chunk B and instead execute a chunk of code EB'.

  • etc. until all possibilities are handled.

This seems complicated but it’s actually fairly straightforward as long as it happens inside a single function. You just have to get used to the fact that errors can make the execution of code jump from one place in the code to a different place.

What happens when there is more than one function involved is the subject of the next reading.

Errors and exceptions

Errors represent "exceptional conditions", which means that they represent a situation which is different from the usual way code executes. Even though the majority of exceptions represent error conditions (file not found, division by zero, indexing off of the end of an array, etc.), not all exceptions represent errors. For instance, exiting a for loop can be signaled by raising a StopIteration exception in an iterator object, which is clearly not an error. (We will discuss this more when we cover user-defined iterator classes.)

Example

Let’s say we want to compute the roots of a quadratic equation. We need to solve this equation for x. There is a standard solution that everyone learns in elementary math courses:

Solve: a*x2 + b*x + c = 0
Solution 1: (-b + sqrt(b2 – 4*a*c)) / (2*a)
Solution 2: (-b - sqrt(b2 – 4*a*c)) / (2*a)

where a, b, and c are numerical parameters. These solutions only work if a is not 0, so there is the potential for error here. Specifically, if a is 0 then you will be dividing by zero, which will result in an error. [2] Let’s write the Python code.

from math import sqrt

def solve_quadratic_equation(a, b, c):
    """Solve the quadratic equation a*x**2 + b*x + c == 0 for x."""
    sol1 = (-b + sqrt(b**2 - 4.0 * a * c)) / (2.0 * a)
    sol2 = (-b - sqrt(b**2 - 4.0 * a * c)) / (2.0 * a)
    return (sol1, sol2)

Problem: What should we do if a is 0? This would make sol1 and sol2 uncomputable (division by zero).

We could always use an if statement to check whether a was 0 before computing anything else. This is not a bad strategy, but usually a will not be 0. We would rather have the code represent the typical case first, and the exceptional cases only when the typical case fails.

So we’ll do this in a different way.

try and except

Python provides a special kind of statement for dealing with errors that can be recovered from: a try/except statement. We’ll show what this looks like in the context of our example, then explain it in detail. Note that if a is 0, then the equation becomes

b*x + c = 0

or:

x = -c / b

(assuming b is nonzero). [3]

We can rewrite the Python code as follows:

 1from math import sqrt
 2
 3def solve_quadratic_equation(a, b, c):
 4    """Solve the quadratic equation a*x**2 + b*x + c == 0 for x."""
 5    try:
 6        sol1 = (-b + sqrt(b**2 - 4.0*a*c)) / (2.0*a)
 7        sol2 = (-b - sqrt(b**2 - 4.0*a*c)) / (2.0*a)
 8        return (sol1, sol2)
 9    except ZeroDivisionError:
10        # a == 0; assume b != 0
11        sol = -c / b
12        return sol

As you can see, the old body of the function is inside the try block on lines 6-8. In addition to the new keyword try, there is another new keyword except which is followed by the name of an exception (here, ZeroDivisionError) and a block of code to be executed in the event that that exception is raised in the code of the try block.

Structure of a try/except statement

try:
    # ... some code which may result in an error ...
except <name of the error>:
    # ... code to run if the error occurs ...

try and except say:

  • Try to execute this block of code (the try block)

  • If a particular error occurs, stop executing the code in the try block and instead execute this other block of code (in the except block)

try and except are block-structured statements (like if, for, and while). You can have multiple statements in a try or except block, all indented the same.

It’s OK to have a try block without an except (though there’s usually no point in doing this). However, you can’t have an except block without a preceding try block (much like you can’t have an else block without a preceding if block.)

Multiple except blocks

You can have multiple except blocks, each corresponding to a different kind of error.

try:
    # ... some code which may result in an error ...
except <error1>:
    # ... code to run if error1 occurs ...
except <error2>:
    # ... code to run if error2 occurs ...

This is a fairly common pattern. We’ll have more to say about it below.

Catch-all except blocks

You can also have a "catch-all" except block, which will execute if any kind of error occurs.

try:
    <some code which may result in an error>
except:
    <code to run if any error occurs>

In general, though, you should avoid using catch-all except blocks. Usually, using a catch-all except is very poor design, because it’s not specific enough to the particular problem. You should be aware of which specific exceptions your code can raise and handle those exact exceptions. Anything else is generally best handled elsewhere or just ignored. If an exception is not handled, it will halt the program, but sometimes that’s what should happen.

One of the few places where a catch-all except block does make sense is after several more specific except blocks at the top level of a program. In this case, the catch-all except block is just there to make sure that the user of the program doesn’t see an uncaught exception.

Terminology

Signaling an error is called raising an exception.

Sometimes we also say throwing an exception, but the term "throw" is more appropriate in languages like Java and C++ which use the throw keyword to signal an exception. Python uses the raise keyword for the same purpose, so in Python it’s better to say "raising an exception".

Handling an error is called catching an exception.

This term also comes from Java and C++, which use the catch keyword instead of except. However, since we can’t say "excepting an exception", Python programmers also say "catching an exception", or sometimes "handling an exception".

Therefore, exceptions are raised in try blocks and caught in except blocks.

Exceptions are objects

Exceptions are actual Python objects. They can have associated data. Often, this data is an error message that indicates more precisely what went wrong.

Exceptions in Python are instances of specific exception classes. Having exceptions as classes is a very powerful feature, because it lets us use Python’s inheritance mechanisms to create exception hierarchies. For instance, one except block can catch any exception which is an instance of a particular exception class or any of its superclasses. (We will discuss this in later readings.)

Some of Python’s built-in exception classes include:

  • ZeroDivisionError (dividing by zero)

  • IndexError (list index out of bounds)

  • ValueError (invalid value of a function argument)

  • TypeError (invalid type of a function argument)

  • FileNotFoundError (e.g. reading a non-existent file)

try/except walkthrough

Let’s look in detail at what happens when an exception is handled. Here’s our example function:

def print_reciprocal(x):
    try:
        recip = 1.0 / x
        print(f'reciprocal of {x} is {recip}')
    except ZeroDivisionError:
        print('Division by zero!')

If we call this with x = 5.0, then recip gets the value 0.2, and no exception is ever raised. The function will print:

reciprocal of 5.0 is 0.2

and that’s it (the except block is never executed).

If we call this with x = 0.0, then as soon as we try to execute the line

recip = 1.0 / x

(or, more specifically, the 1.0 / x part of the line), a ZeroDivisionError exception will be raised. When this happens, the rest of the code in the try block (including the assignment of 1.0 / x to recip) will be skipped, and execution will jump out of the try block, looking for an except block that can handle a ZeroDivisionError. Fortunately, there is one right after the try block, so execution will resume with the code in the except ZeroDivisionError: block, and the program will print:

Division by zero!

At this point, the exception has been caught and "recovered from". As you can see, the meaning of "error recovery" is whatever the programmer wants it to be; it’s whatever happens after the exception is caught.

After the code in the except block is executed, execution will resume with whatever code is after the try/except block. In this case, there isn’t any, so the function will return.

To recap: as soon as an exception is raised in a try block, the program jumps out of the try block and looks for an except block that corresponds to that type of exception. If it finds one, it executes the code in that block. If it doesn’t... we’ll get to this later.

Multiple except blocks walkthrough

As we mentioned above, a try block can be followed by multiple except blocks. Each except block should correspond to a different type of exception. We can also have a catch-all except block at the end, though it’s usually better to just leave it out.

Here’s a simple example of multiple except blocks:

values = [-1, 0, 1]
for i in range(5):
    try:
        r = 1.0 / values[i]
        print(f'reciprocal of {values[i]} is {r}')
    except IndexError:
        # when i > 2
        print(f'index {i} out of range')
    except ZeroDivisionError:
        print('division by zero')

This code can raise (and catch) two kinds of exceptions: IndexError and ZeroDivisionError.

i == 0

values[0] is -1, so computing the reciprocal will not raise any exceptions. Therefore, this prints:

reciprocal of -1 is -1.0

(The .0 in -1.0 is because the number is a float.)

i == 1

values[1] is 0, so computing the reciprocal will raise a ZeroDivisionError. This will cause execution to leave the try block and enter the except ZeroDivisionError: block. Therefore, this will print:

division by zero

i == 2

This prints:

reciprocal of 1 is 1.0

You should be able to figure out why.

i == 3

values[3] is off the end of the values list, so accessing it will result in an IndexError being raised. This will cause execution to leave the try block and enter the except IndexError: block. Therefore, this will print:

index 3 out of range

as will the i == 4 case.

When evaluating a try/except statement, only one except statement’s code can be executed. When an exception is raised, Python tries each except block in order, and executes the first one which "matches" the exception.

"Matching an exception" usually means that the exception raised is the same as the exception referred to in the except line, but with inheritance (covered in a future reading), some exceptions can match more than just the exception that was raised. Catch-all except blocks, of course, match all exceptions.

Even if there is a catch-all except block at the end, it’s not executed if a previous except block’s code was executed first. To illustrate, let’s change the previous code a bit by adding a catch-all except block:

values = [-1, 0, 1]
for i in range(5):
    try:
        r = 1.0 / values[i]
        print(f'reciprocal of {values[i]} is {r}')
    except IndexError:
        # when i > 2
        print(f'index {i} out of range')
    except ZeroDivisionError:
        print('division by zero')
    except:
        print('some other exception happened')

In the example cases:

  • if an IndexError exception occurs, only the code in the except IndexError: block gets executed;

  • if a ZeroDivisionError exception occurs, only the code in the except ZeroDivisionError: block gets executed;

  • if some other exception occurs, only the code in the catch-all except: block gets executed.

Different except blocks are mutually exclusive!

Final example

When a file is opened for reading, but the file does not exist, a FileNotFoundError exception is raised. This is very common when e.g. a user inputs the wrong file name. How can we handle this in our code? Let’s assume that our files contain numbers, one per line. We want to add them all up and print their sum.

First attempt

def sum_numbers_in_file(filename):
    sum_nums = 0
    file = open(filename, 'r')
    for line in file:
        line = line.strip()
        sum_nums += int(line)
    file.close()
    return sum_nums

This will work fine as long as a file with the given name exists. If not, a FileNotFoundError will be raised, which will halt the program. Can we do better?

Second attempt

def sum_numbers_in_file(filename):
    sum_nums = 0
    found_file = False
    while not found_file:
        try:
            file = open(filename, 'r')
            found_file = True
        except FileNotFoundError:
            print('File not found!')
            filename = input('Enter another filename: ')
    for line in file:
        # ... (as before)

open will raise a FileNotFoundError if the file isn’t found. Code in the except block will be executed next. The user will be prompted for a new filename, then the code will try to open that file, etc. until a file can be opened for reading.

Once open succeeds, no exception will be raised. file will be a file object, found_file will be set to True, and the while loop will terminate. Then the rest of the code will execute as before. (It’s still possible that a ValueError exception will be raised if a line in the file hasn’t got the right kind of contents; we’ll look at that case in the next reading.)

This approach is still pretty ugly. What if we had a very long list of filenames and wanted to sum all the numbers in all the files? If there are lots of filenames, it’s not practical to prompt the user every time a file doesn’t exist. It would be nice if we could do error handling outside of this function. This is the subject of the next reading. We will revisit this example and show you the right way to design the function so that you can handle exceptions in whichever way makes sense for the specific application you have in mind.

Next reading

We’ll talk about catching an exception outside of the function it was raised in. This will lead us to a discussion of the runtime stack, and what a traceback means.


[End of reading]


1. Tracebacks can be much more complicated than this, as we’ll see in the next reading. They don’t just show you where the error occurred, but also the names of all of the partially-completed functions that were active when the error occurred.
2. There is also the possibility of getting complex numbers as answers, which we’ll ignore for now.
3. If both a and b are zero, then you either have no solutions (if c isn’t zero) or an infinite number of solutions (if c is zero). We won’t worry about that case here.