Exceptions in Python

Like C# and Java, Python uses special objects called exceptions to manage runtime errors. A runtime error is something that is beyond the developer’s control, such as a missing file.

When such a condition occurs, Python raises an appropriate exception with related information for diagnosis. To raise an exception involves unwinding the application call stack until an appropriate exception handler is found. If no such handler is found, the program exits with a Traceback error.

For example, here I am trying to open a file which does not exist:

with open('yikes.txt', 'r') as f:
    print(f.read())

Since the file yikes.txt is missing, Python will raise the exception FileNotFoundError, and because I have no appropriate exception handler the program exits with the following Traceback message:

This error is very useful as it details where the error occurred, what we were trying to do, and what the specific problem was. As useful as the error is, for the end user it’s nothing short of a disaster. How can we prevent our program from crashing like this?

Basics of Exception Handling

In order to handle exceptions, you need to catch them, using a try/except block:

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except FileNotFoundError:
    print('The yikes file is missing')

In the above example we instruct Python to try open the file and print its contents, if the FileNotFoundError exception occurs, print an error message. If you are familiar with C# or Java, you’ll recognise the try/catch statement. It is possible a different type of exception occurs, perhaps there is an error reading it. We can handle that too:

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except FileNotFoundError:
    print('The yikes file is missing')
except IOError:
    print('Unable to read the yikes file')

In our example, if FileNotFoundError occurs, then Python will enter that exception handler. If IOError occurs, that exception block will be executed.

The built-in exception classes are defined in a hierarchy. FileNotFoundError is a descendant of IOError. Consequently, when using them in the try statement, always declare the more specific exceptions first followed by the more general ones (the base classes).

Consider the following example where the IOError handler is declared first. If a FileNotFoundError occurs the IOError handler is executed, because a FileNotFoundError is a IOError:

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except IOError:
    print('Unable to read the yikes file')
except FileNotFoundError:
    // this exception handler code will never execute
    print('The yikes file is missing')

Incidentally, we can group exception handlers:

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except (FileNotFoundError, IOError):
    print('The yikes file is missing')

But since these two are of the same hierarchy, we can just use the base class IOError:

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except IOError:
    print('The yikes file is missing')

Let’s print out the details of the exception, to do that we need to assign it to a variable:

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except IOError as e:
    print("The following error occurred reading yikes.txt")
    print(e)

# outputs:

The following error occurred reading yikes.txt
[Errno 2] No such file or directory: 'yikes.txt'

It is possible to examine the arguments of an exception instance:

try:
    raise NameError("user name", "invalid characters")
except NameError as e:
    name, message = e.args
    print("name:  ", name)
    print("error: ", message)

# outputs:

name:   user name
error:  invalid characters

Python Exception classes can accept a tuple of arguments (*args):

try:
    raise NameError("user name", "invalid characters", 1234, "code: 102")
except NameError as e:
    print(e.args)

# output: 

('user name', 'invalid characters', 1234, 'code: 102')

Raising an Exception

In order to throw an exception in Python, we use the raise statement:

raise NameError("invalid user name")

This is similar to the throw statement in languages like C# and Java.

Re-raising a Handled Exception

Perhaps we prefer to log our error and then re-throw it, deciding to crash early rather than continue in an unrecoverable state, we can do that with the raise statement :

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except IOError as e:
    my_logger.error("The following error occurred reading yikes.txt")
    my_logger.error(str(e))
    raise

Exception Chaining

It’s also possible to throw a different error from the existing error, this is known as nesting:

try:
    with open('yikes.txt', 'r') as f:
        print(f.read())
except IOError as e:
    raise RuntimeError("Failed to open yikes.txt") from e

We can see the details of both exceptions in our Traceback message:

Again, if you come from a C# background, you might be familiar with inner exceptions, this is the same thing. Moving on, let’s put our code into a function and demonstrate a couple more features of Python exception handling:

def display_yikes():
    try:
        with open('yikes.txt', 'r') as f:
            print(f.read())
    except FileNotFoundError:
        print('The yikes file is missing')
    except IOError as e:
        print('Unable to read the yikes file')
        raise RuntimeError("Failed to open yikes.txt") from e
    except Exception as e:
        print('An unknown exception occurred.')
        print(str(e))
        raise
    else:
        // if no exceptions occurred, this code runs
        print("that's it for today.")
    finally:
        // this code always runs last
        print("check again tomorrow for more daily yikes...")

display_yikes()

Here we can see that:

  • we catch any unhandled exceptions that inherit from the base class Exception
  • If no exceptions occurred in the try statement, the code in the else clause executes
  • any code in the finally clause always executes last, irregardless of whether an exception occurred or not – even if we re-raised an exception or it simply wasn’t caught.

Creating Exception Classes

To create a new exception class in Python, inherit from the desired exception base class:

class UsernameError(NameError):
    def __init__(self, name, message):
        self.name = name
        self.message = message


raise UsernameError('bob@mondo', 'only alphabetic characters allowed')

Exception Hierarchy

Finally, here are a few of the classes in Python’s exception hierarchy:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s