Introductory Programming in Python: Lesson 25
Flow Control: Exceptions

[Prev: Understanding Python's Error Messages] [Course Outline] [Debugging]

The Nature of Errors

In an ideal world there would be no errors. As programmers we are lazy, smart, and for some reason, despite copious amounts of blatant evidence to the contrary, idealistic. We like to believe that errors are unusual. We consider them exceptional. Something out of the ordinary. Hence the term exception was coined. An exception is an error, but it's more than that. When an error occurs we want to know why it occurred, and what the error is. If errors were simply errors, each one would be different, and we would have to deal with each one individually, when it was encountered. But we realise quickly that errors can be partitioned into groups. Some errors are related to input/output, others to mathematical violations, yet others to syntax violations etc... So we start thinking classes. Let's put our errors into classes, and handle classes of errors. Let's also call errors exceptions for the sake of optimism. So exceptions are classes of errors. Exceptions can thus be derived from parent classes. This means we can deal only with a higher level class such as 'all I/O exceptions' or with a specific class of exception ('file not found') as and when we see fit. We can also create our own specialised exceptions, by creating a class derived from another exception. The base exception class is, surprisingly, called 'Exception'.

Catching Exceptions with the try Statement

Great! So now we have this wonderful hierarchy of exceptions. What do we do with them? When things go wrong in our program, exceptions are thrown. The exception is thrown outwards in terms of scope. At each level of scope, it can be caught. If an exception is uncaught by the time it hits the global scope, python catches it. But python does nasty things when it catches exceptions. It prints out tracebacks and stops running our program. So we want to catch errors before they get to the global scope. To do this we use the try statement.

try:
    <statement>
    [statement]
    [...]
except [varname] [exception class]:
    <statement>
    [statement]
    [...]
[except [varname] [exception class]:
    <statement>
    [statement]
    [...]]

Each statement in a try block is executed in order, as normal. If any exceptions are thrown, execution immediately jumps to the beginning of the first except block with a matching exception class. So if the exception thrown is an ArithmeticError, execution jumps to the except ArithmeticError: block. If there is a division by zero exception, which is a subclass of ArithmeticError, and there is an except ZeroDivisionError: clause before the ArithmeticError clause, execution will jump there. Once execution has reached an except clause, the exception in considered caught, and won't continue outwards to the global scope. Of course, if in our handling of the exception in the except clause, we throw another exception, then we must catch that too, or have python get rude on us.

#a small program to demonstrate exception handling

import math

try:
   print 3.2/0.0
except ZeroDivisionError:
   print "Division by Zero!"
except ArithmeticError:
   print "We'll never get here because ZeroDivisionError is a subclass of ArithmeticError"
#a small program to demonstrate exception handling

import math

try:
   print math.sqrt(-1)
except ZeroDivisionError:
   print "Division by Zero! But sqrt(-1) isn't this kind of error, so we'll skip this."
except ValueError:
   print "Use a complex number instead!"

Note how we can have multiple except clauses for a single 'try statement'. And also note how the order of exceptions of the same (super)class need to be ordered correctly. Finally, we can just have a classless except clause after all our other except clauses (except:), which will catch any and every exception that falls through to it.

Cleaning up with finally

Sometimes, we want to do something regardless of whether an exception was encountered or not. For example, we have a section of code that opens a file and parses it. If during parsing we encounter an exception, we must close the file. But we need to close the file even if parsing was successful. A single 'finally clause' can be placed after all 'except clauses' in a try statement. The code of the finally clause is executed after all of the main clauses code and any except clause code that needs to be executed. Note that the finally clause can only be used in conjuntion with an except clause from python 2.5 onwards. Before python 2.5 try statements can have either an except clause or a finally clause, not both.

Throwing Our Own Tantrums

The last bit about exceptions, is how to be objectionable, err... exceptional, ourselves. When we are writing code to put into a module, for later use, we often wish to throw our own exceptions when we find strange conditions, to alert whoever else is using our code to a problem. We can do so using the raise statement.

raise <exception object>

Note that the raise statement takes an object, not a class, so we must instantiate an object of the chosen exception class. For example, in a vector classes vector addition method, we might want to raise an exception if the second vector is not of the same length as the first. We would raise a ValueError (the second vector doesn't have an appropriate value) and we do so using

raise ValueError("Vector's must be of the same length for addition")

Above we see two things. First the use of round brackets to 'call the class', or rather create an object of that class. Secondly, we see a feature of all exception classes constructors, namely the ability to create an exception object with a specific error message. Python will display this error message at the end of the traceback if the exception remains uncaught.

A Monolithic Example in the Form of a FastA Parser

#This is a module that provides a FastA file parser. It serves to demonstrate the
#use of exceptions using try, except, raise, and finally

class ParseError(Exception): #Create a new class of exception
    pass

def getFastARecord(f):
    """Retrieves the next FastA record from the already opened file f. 
       Return a tuple (sequence title, sequence string) 
       or None if at end of file. Raises a ParseError"""
    line = f.readline()
    while line.isspace() and line != '':
        line = f.readline()
    if line == '':
        return None
    if not line.startswith('>'):
        raise ParseError("Expected '>' at start of next sequence record. "+
        "Is this a FastA file?")
    title = line[1:-1]
    seq = ""
    line = f.readline()

    #this uses the strict format of a fasta file which requires at least 
    #one empty line between records
    while (not line.isspace()) and (line != ''): 
        seq += line.rstrip()
        line = f.readline()
    return (title, seq)

def getAllFastARecords(path):
    """Attempts to open the file path, the returns a list of all FastA records in the file 
       as tuples of the form (title, sequence string)."""

    try:
        f = open(path, "r");
    except IOError: 
        #here we catch an IOError 
        #(i.e. the file doesn't exist, can't be opened, is corrupt, 
        #we don't have permissions etc...)
        return None

    #from here we know f has been successfully opened, so it must be closed
    try:
        records = []
        rec = getFastARecord(f)
        while rec != None:
            records.append(rec)
            rec = getFastARecord(f)
    except ParseError: #if there is a parse error return no records
        return None
    finally:
        f.close()

    return records

Exercises

[Prev: Understanding Python's Error Messages] [Course Outline] [Debugging]