I l@ve RuBoard Previous Section Next Section

7.4 Exception Catching Modes

Now that we've taken a first look, let's fill in a few details behind Python's exception model.

7.4.1 try Statement Clauses

When you write try statements, a variety of clauses can appear after the try statement block; Table 7.1 summarizes all the possible forms. We've already seen most of these in the previous examples—empty except clauses catch any exception, finally runs on the way out, and so on. There may be any number of excepts, but finally must appear by itself (without an else or except), and there should be only one else in a try.

Table 7.1. try Statement Clause Forms

Clause Form

Interpretation

except:

Catch all (other) exception types

except name:

Catch a specific exception only

except name, value:

Catch exception and its extra data

except (name1, name2):

Catch any of the listed exceptions

else:

Run block if no exceptions raised

finally:

Always perform block

7.4.2 Catching 1-of-N Exceptions

The fourth entry in Table 7.1 is new. except clauses can also provide a set of exceptions to be caught, in parentheses; Python runs such a clause's statement block if any of the listed exceptions occur. Since Python looks for a match within a given try by inspecting except clauses from top to bottom, the parenthesized version is like listing each exception in its own except clause, except that the statement body needs to be coded only once.

Here's an example of multiple except clauses at work. In the following, when an exception is raised while the call to the action function is running, Python returns to the try and searches for the first except that catches the exception raised. It inspects expect clauses from top to bottom and left to right, and runs the statements under the first that matches. If none match, the exception is propagated past this try; the else runs only when no exception occurred. If you really want a catch-all clause, an empty except does the trick:

try:
    action()
except NameError:
    ... 
except IndexError
    ...
except KeyError:
    ...
except (AttributeError, TypeError, SyntaxError):
    ...
else:
    ...

7.4.3 Exceptions Nest at Runtime

So far, our examples have used only a single try to catch exceptions, but what happens if one try is physically nested inside another? For that matter, what does it mean if a try calls a function that runs another try? Both these cases can be understood if you realize that Python stacks try statements at runtime. When an exception is raised, Python returns to the most recently entered try statement with a matching except clause. Since each try statement leaves a marker, Python can jump back to earlier trys by inspecting the markers stack.

An example will help make this clear. The following module defines two functions; action2 is coded to trigger an exception (you can't add numbers and sequences), and action1 wraps a call to action2 in a try handler, to catch the exception. However, the top-level module code at the bottom wraps a call to action1 in a try handler too. When action2 triggers the TypeError exception, there will be two active try statements—the one in action1, and the one at the top level of the module. Python picks the most recent (youngest) with a matching except, which in this case is the try inside action1. In general, the place where an exception winds up jumping to depends on the control flow through a program at runtime:

def action2():
    print 1 + []           # generate TypeError

def action1():
    try:
        action2()
    except TypeError:      # most recent matching try
        print 'inner try'

try:
    action1()
except TypeError:          # here only if action1 reraises
    print 'outer try'

% python nestexc.py
inner try

7.4.4 finally Clauses Run "On the Way Out"

We've already talked about the finally clause, but here's a more sophisticated example. As we've seen, the finally clause doesn't really catch specific exceptions; rather, it taps into the exception propagation process. When used, a finally block is always executed on the way out of a try statement, whether the exit is caused by an exception or normal completion of the statements in the try block. This makes finally blocks a good place to code clean-up actions (like closing files, as in the previous example).

The next code snippet shows finally in action with and without exceptions. It defines two functions: divide, which may or may not trigger a divide-by-zero error, and tester, which wraps a call to divide in a try/finally statement:

def divide(x, y):
    return x / y                         # divide-by-zero error?

def tester(y):
    try:
        print divide(8, y)
    finally:
        print 'on the way out...'

print '\nTest 1:'; tester(2)
print '\nTest 2:'; tester(0)             # trigger error

% python finally.py

Test 1:
4
on the way out...

Test 2:
on the way out...
Traceback (innermost last):
  File "finally.py", line 11, in ?
    print 'Test 2:'; tester(0)
  File "finally.py", line 6, in tester
    print divide(8, y)
  File "finally.py", line 2, in divide
    return x / y                       # divide-by-zero error?
ZeroDivisionError: integer division or modulo

Now, the module's top-level code at the bottom calls tester twice:

Why You Will Care: Lazy Programs

One way to see why exceptions are useful is to compare coding styles in Python and languages without exceptions. For instance, if you want to write robust programs in the C language, you have to test return values or status codes after every operation that could possibly go astray:

doStuff()
{                                  // C program:
     if (doFirstThing() == ERROR)  // must detect errors everywhere
          return ERROR;            // even if not processed here
     if (doNextThing() == ERROR)
          return ERROR;
     ...
     return doLastThing();
}

main()
{
     if (doStuff() == ERROR)
          badEnding();
     else
          goodEnding();
}

In fact, realistic C programs have as much code devoted to error detection as to doing actual work. But, in Python, you don't have to be so methodical; instead you can wrap arbitrarily vast pieces of a program in exception handlers and write the parts that do the actual work to assume all is well:

def doStuff():
     doFirstThing()           # we don't care about exceptions here
     doNextThing()            # so we don't need to detect them here
     ...
     doLastThing()

if__name__ == '__main__':
     try:
          doStuff()           # this is where we care about the result
     except:                  # so it's the only place we need to check
          badEnding()
     else:
          goodEnding()

Because control jumps immediately and automatically to a handler when an exception occurs, there's no need to instrument all your code to guard for errors. The upshot is that exceptions let you largely ignore the unusual cases and avoid much error-checking code.

I l@ve RuBoard Previous Section Next Section