Exceptions

Indices and tables

There are many situations where the normal flow of your program needs to be interrupted to deal with an unusual or exceptional condition. Quite often, you can’t predict when (in time) or where (in your code) these exceptional conditions will occur. For example:

  • The user presses CTRL-C on the keyboard
  • The disk you are trying to write to becomes full
  • Your program attempted to divide something by 0.

Python pre-defines a lot of exceptions, which can be found here. When one of these exceptional conditions is detected, Python raises an exception at the point at which it detects the problem. It is also possible for you to purposely raise exceptions from within your code.

Exceptions are not automatically fatal as Python provides syntax to trap for and handle them.

The try Statement

The try statement is used to setup exception handling around a group of statements that you want to protect.

It looks generally like the following [1]:

try:
   try_suite
except exception_group1 as variable1:
   except_suite1
...
except exception_groupN as variableN:
   except_suiteN
else:
   else_suite
finally:
   finally_suite

Let’s break down the various parts:

The try and try_suite

Required: Yes

try:
   try_suite
except exception_group1 as variable1:
   except_suite1
...
except exception_groupN as variableN:
   except_suiteN
else:
   else_suite
finally:
   finally_suite
  • This is where the protected statements are placed. If an exception occurs while this suite of code is being executed, the rest of the statements in the suite are skipped and Python searches for an exception handler to handle it.

The except, variable and except_suite

Required: except and except_suite -> Yes (1 or more); variable -> no

try:
   try_suite
except exception_group1 as variable1:
   except_suite1
...
except exception_groupN as variableN:
   except_suiteN
else:
   else_suite
finally:
   finally_suite
  • This is where you specify the class name of the exception(s) to trap for and the handling code to execute for each. Can be:

    • A single exception, or,
    • A tuple of exceptions, or,
    • Empty, which traps for all exceptions (for obvious reasons, be careful to put this as the last except clause).

For example:

except KeyboardInterrupt as ex:
    pass
except (OSError, EOFError) as ex:
    pass
except:
    # Put empty 'except' clause last
    # as it traps for everything else
    pass
  • When an exception is thrown, the except statements are searched in-turn until a matching one is found.
  • Exceptions that are thrown will match an except clause if the exception given in the except clause is the same class as, or base class of, the exception that was thrown.
  • If the as keyword is specified, the exception object is made available to the handler by the name except.
  • Once an except clause is matched, the search is terminated. If no matching except clause is found, the search continues in the surrounding code blocks.

Note

If no exception handler is found by the time the top of the program is reached, this is considered an unhandled exception and the program is terminated. When the program is terminated this way, a stack trace is printed to the screen to assist with debug. This is often viewed as “dirty” by users of your program because it is not only cryptic and difficult for most people to read and interpret, but, it also exposes a hole in your software you didn’t protect against.

  • If the exception is handled, program execution continues into the finally block (if present) and then off the end of the try statement.
  • If the exception handler causes another exception, execution of the except_suite is stopped, the original exception is nested inside the new exception via the new exception’s __context__ attribute and a search is started fo find a handler for the new exception.

Tip

Generally speaking, don’t trap for exceptions unless you intend to do something about it. Trapping it but doing nothing is useless. Trapping it and re-throwing it is useless.

The else and else_suite

Required: No

try:
   try_suite
except exception_group1 as variable1:
   except_suite1
...
except exception_groupN as variableN:
   except_suiteN
else:
   else_suite
finally:
   finally_suite
  • If present, the else_suite is executed if no exception occurs. Otherwise, it is skipped.
  • If an exception occurs in the else clause, the preceding except clauses are not searched again. The exception must be handled by an outer exception trap.

Tip

Use an else clause when you want code to not be protected by the handlers defined in the current try statement (i.e. you want the code protected by a handler created outside the try statement.).

The finally and finally_suite

Required: No

try:
   try_suite
except exception_group1 as variable1:
   except_suite1
...
except exception_groupN as variableN:
   except_suiteN
else:
   else_suite
finally:
   finally_suite
  • Is always executed, whether an exception happens or not. If there is no exception handler in the try structure to handle the current exception, the finally block is still executed and the exception subsequently re-raised.

Tip

Use a finally block to allow the program to do things like clean up after itself (for example, release file handles and close network sockets gracefully).

  • If you use a break, continue or return statement in a try suite, the finally clause is still executed. A return statement is therefore useless in the try suite if a return value is also present in the finally suite because the value returned is always from the last return statement executed. For example:
>>> def foo():
...    try:
...       return 'Hi'
...    except Exception as ex:
...       pass
...    finally:
...       return "Bye"
>>> foo()
'Bye'
  • If the finally_suite causes another exception, execution of the finally_suite is stopped, the original exception is nested inside the new exception via the new exception’s __context__ attribute and a search is started fo find a handler for the new exception.

Control Flow Examples

The diagrams in Fig. 33, Fig. 34 and Fig. 35 illustrate the various control flow patterns that the try statement supports.

../../_images/normal_control_flow.png

Fig. 33 Normal Control Flow

../../_images/handled_exception_control_flow.png

Fig. 34 Handled Exception Control Flow

../../_images/unhandled_exception_control_flow.png

Fig. 35 Unhandled Exception Control Flow

Exception Arguments

Exceptions have “associated values” (a.k.a. the “exception arguments”) which frequently contains information about the cause of the exception. Most exceptions take at least one, optional, input object (like a number or a string as a message). Exactly what information is provided is exception dependent. The description of the built-in exception classes talks about what extra information is available in them.

If the interpreter creates the exception, it populates the associated values.

If your program purposely raises an exception, you can set those values when you call the exception objects constructor to create it.

If you try and print an exception object, the exceptions associated values is typically what is printed.

For example the following code:

fh = None
try:
    fh = open('asdf.txt')
except OSError as ex:
    print("Oh no...something went wrong. Here's what it is:")
    print(ex)
finally:
    if fh is not None:
        fh.close()

Returns the following when printing the exception object:

Oh no...something went wrong. Here's what it is:
[Errno 2] No such file or directory: 'asdf.txt'

Raising Exceptions

Your program can throw an exception by executing the raise statement, which has the following general syntax:

raise exception(*args)

Raises the given exception, which can be either an exception class name, or, an instance of an exception class.

For example:

def func():
    print("FUNC: Doing some stuff...")
    print("FUNC: *** Raising an exception ***")
    raise AttributeError("Do you have the right duck?")
    print("FUNC: More stuff...")

try:
    print("TRY : Executing try...")
    func()
except AttributeError as ex:
    print("EXC : Got an exception. Handling it.")
    print("EXC :    {}".format(type(ex)))
    print("EXC :    {}".format(ex))
finally:
    print("FIN : Done try")

Produces the following output:

TRY : Executing try...
FUNC: Doing some stuff...
FUNC: *** Raising an exception ***
EXC : Got an exception. Handling it.
EXC :    <class 'AttributeError'>
EXC :    Do you have the right duck?
FIN : Done try

If you truly want to just re-raise an exception that you trapped, you can just type raise.

def func():
    print("FUNC: Doing some stuff...")
    print("FUNC: *** Raising an exception ***")
    raise AttributeError("Do you have the right duck?")
    print("FUNC: More stuff...")

try:
    print("OTRY: Executing outer try...")
    # NOTE: Nested try block
    try:
        print("ITRY: Executing inner try...")
        func()
    except Exception as ex:
        print("IEXC: Got an exception, but, can't handle it.")
        print("IEXC: Re-raising...")
        raise
    finally:
        print("IFIN: Done inner try")
except AttributeError as ex:
    print("OEXC: Got an exception. Handling it.")
    print("OEXC:    {}".format(type(ex)))
    print("OEXC:    {}".format(ex))
finally:
    print("OFIN: Done outer try")

Produces the following output:

OTRY: Executing outer try...
ITRY: Executing inner try...
FUNC: Doing some stuff...
FUNC: *** Raising an exception ***
IEXC: Got an exception, but, can't handle it.
IEXC: Re-raising...
IFIN: Done inner try
OEXC: Got an exception. Handling it.
OEXC:    <class 'AttributeError'>
OEXC:    Do you have the right duck?
OFIN: Done outer try

Custom Exception Classes

Exceptions are objects and new object classes are created by deriving from a base class. In Python, new exceptions are created by deriving from the Exception class, or, one of its sub-classes. The hierarchy of built-in exceptions is found here.

Tip

When creating your own exception class the name should usually end in ‘Error’ (unless of course it actually doesn’t represent an error).

For example a new exception can be defined as simply as:

>>> class MyException(Exception): pass

And then you can use it in your program:

def func():
    print("FUNC: Doing some stuff...")
    print("FUNC: *** Raising an exception ***")
    raise MyException("So do you like, stuff?")
    print("FUNC: More stuff...")

try:
    print("TRY : Executing try...")
    func()
except MyException as ex:
    print("EXC : Got an exception. Handling it.")
    print("EXC :    {}".format(type(ex)))
    print("EXC :    {}".format(ex))
finally:
    print("FIN : Done try")
TRY : Executing try...
FUNC: Doing some stuff...
FUNC: *** Raising an exception ***
EXC : Got an exception. Handling it.
EXC :    <class '__main__.MyException'>
EXC :    So do you like, stuff?
FIN : Done try

Exception classes can do what any other class can do (execute specific code on construction, define additional properties etc). However, that will be covered in the section on Classes.

Performance Advantages

A common coding style in Python embraces the notion that it is easier to ask for forgiveness than permission. The try statement supports this style.

Consider for example, the two approaches to opening a file:

  • Ask for permission: Check if the file exists and that you have permissions to read it prior to trying to open the file. Implies use of the if statement.
  • Beg for forgiveness: Try to open the file, if there is an exception generated from it being a non-existent file or insufficient user permissions, handle it gracefully. Implies use of the try statement.

We can test the performance difference between these two styles using the code contained in this notebook which declares two life form classes, and asks them to make their characteristic sound if they can.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class duck(object):
    # Can make a sound
    def make_sound(self):
        print("QUACK!")

class fish(object):
    # Can't make a sound
    pass

def ask_permission(obj):
    if hasattr(obj, "make_sound"):
        obj.make_sound()
    else:
        print("Life form makes no sound.")

def beg_forgiveness(obj):
    try:
        obj.make_sound()
    except AttributeError:
        print("Life form makes no sound.")

The performance results from running this code 1000x, and averagaed over multple runs, are found here and summarized in Table 16:

Table 16 Table of Exception Testing Results
Style Makes Sound (Duck) Sound-less (Fish)
Ask Permission (IF) 0.002834s 0.003065s
Beg Forgiveness (TRY) 0.002676s 0.003300s
Difference -0.0001575s 0.0002355s
Winner BEG (IF) ASK (TRY)

The performance take-aways from this table are the following:

  • The “IF” version always costs you. The more costly the check (for example checking a network drive vs checking a local drive), the greater the performance penalty compared to the “TRY” version.
  • The “TRY” version only costs you if an exception is actually thrown (and then it can be quite costly).

So, if the chance of an exception is high, then use the “IF” version. But if the exception is infrequent (i.e. truly exceptional), use the “TRY” version.

From a code construction point of view (applies to all applications) the “TRY” version is generally favored overall because:

  • The “IF” version has you wind up with a lot of “if error” in your code which can affect readability and maintainability.
  • The “TRY” version cleanly separates the business logic from the error logic.

General suggestions:

  • By default wrap your code with TRY statements to improve readability and maintainability.
  • If you have performance issues, investigate why and selectively convert to the “IF” based structure as warranted.
[1]Syntax representation re-used from, Programming in Python3, Mark Summerfield, 2009, Addison-Wesley.