Python Exceptions: Part Four

exceptionsPython Exceptions: try/except/finally

In all versions of Python prior to Release 2.5, there were two types of try statements. You could either use a finally to ensure that cleanup code was always run, or write except blocks to catch and recover from specific exceptions and optionally specify an else clause to be run if no exceptions occurred. In other words, the finally clause could not by mixed with except and else.

That has changed with Python 2.5 and later. Now, the two statements have merged; we can mix finally except and else clauses in the same statement:

try:
	statements
except Exception1:
	handler1
except Exception2:
	handler2
else:
	else_block

The code in this statement’s main-action block is executed first, as usual. If that code raises an exception, all the except blocks are tested, one after another, looking for a match to the exception raised. If the exception raised is Exception1, the handler1 block is executed; if it’s Exception2, handler2 is run, etc. If no exception is raised, the else-block is executed.

No matter what’s happened previously, the finally-block is executed once the main action block is complete and any raised exceptions have been handled. In fact, the code in the finally-block will be run even if there is an error in an exception handler or the else-block and a new exception is raised.

As always, the finally clause does not end the exception. If an exception is active when the finally-block is executed, it continues to be propagated after the finally-block runs, and control jumps somewhere else in the program. If no exception is active when the finally is run, control resumes after the entire try statement.

The effect here is that the finally-block is always run, regardless of whether [1] an exception occurred in the main action and was handled; [2] an exception occurred in the main action and was not handled; no exceptions occurred in the main action, and/or [4] a new exception was triggered in one of the handlers. Again, the finally serves to specify cleanup actions that must always occur on the way out of the try, regardless of what exceptions have been raised or handled.

When try, except, else and finally are combined like this, the order must be like this:

	try -> except -> else -> finally

where the else and finally are optional, and there may be zero or more except blocks, but there must be at least one except if an else appears. The try statement essentially consists of two parts: excepts with an optional else, and/or the finally.

Because of these rules, the else can appear only if there is at least one except, and it is always possible to mix except and finally, regardless of whether an else appears to though the except can omit an exception name to catch everything and run a raise statement to re-raise the current exception. If you violate any of these rules, Python will raise a syntax error exception before your code runs.

Finally, prior to Python 2.5, it is actually possible to combine finally and except clauses in a try by syntactically nesting a try/except in the try block of a try/finally statement. The following has the same effect as the new merged form:

try:
	try:
		main-action
	except Exception1:
		handler1
	except Exception2:
		handler2
	...
	else
		no-error
	finally:
		cleanup

Again, the finally block is always run on the way out, regardless of what happened in the main action and regardless of any exception handlers run in the nested try. Since an else always requires an except, this nested form even sports the same mixing constraints of the unified statement form outlined in the preceding section. But this nested equivalent is more obscure and requires more code than the new merged form. Mixing finally into the same statement makes your code easier to write and read and is thus the generally preferred technique.

External Links:

Handling Exceptions at Python Wiki Built-in Exceptions at docs.python.org

Python Exceptions: Part One

exceptionsExceptions are events that can modify the flow of control through a program; they are triggered automatically on errors, and can be triggered and intercepted by your Python code. Python has the following statements to deal with exceptions:

  • try/except: Catch and recover from exceptions raised by Python (or by you)
  • try/finally: Perform cleanup actions, whether exceptions occur or not.
  • raise: Trigger an exception manually in your code.
  • assert: Conditionally trigger an exception in your code.
  • with/as: Implement context managers in Python 2.6/3.0.

Exceptions let us jump out of large chunks of a program (whatever is included in the “try” block). The try statement works as follows:

  • First, the try clause is executed.
  • If no exception occurs, the except clause is skipped and execution of the try statement is finished.

To provide an example, let’s assume we have a character string:

>>> myStr = ‘Hello, world!’
>>> print(myStr[3])
l

If we reference an index within the bounds of the string, then the code works as it should, and we get no error. But if we specify an index outside of the bounds of the string, we get a different result:

Traceback (most recent call last):
File “<pyshell#3>”, line 1, in
print(myStr[13])
IndexError: string index out of range

Handling Exceptions

In our programs, we can let an error occur, in which case the program stops running, or we can try to handle it. The error generated by trying to access myStr[13] was an IndexError, so we will handle the IndexError exception in our code:

def testException():
	myStr = 'Hello, world!'
	try:
		print(myStr[13])
		print('No errors so far.')
	except IndexError:
		print('Index error.')
	print('Continuing...')

In this code, we have a try/except block. In try, we attempt to print out myStr[13]. If there is no error, we will move on to the next line (and print out ‘No errors no far.’). If an IndexError exception is raised, we will jump immediately to the except statement. The last line (print out ‘Continuing…’) will execute whether or not an exception was raised. Not surprisingly, we get the following output when we run the function:

>>> testException()
Index error.
Continuing…

Note that if the program generated an exception that was not an IndexError, the exception would be unhandled and the program would terminate with an error. However, a try statement may have more than one except clause, to specify handlers for different exceptions. At most one handler will be executed. Handlers only handle exceptions that occur in the corresponding try clause, not in other handlers of the same try statement. An except clause may also name multiple exceptions as a parenthesized tuple. For example:

except (IndexError, ValueError, RuntimeError):
pass

In this case, the last except clause may omit the exception name or names to serve as a wildcard. You can use this to print an error message and re-raise the exception to allow the caller to handle the exception as well. For example, we could rewrite the above function:

def testException():
    myStr = 'Hello, world!'
    try:
        print(myStr[12])
        print('No errors so far.')
        a = int(myStr[12])
    except IndexError:
        print('Index error.')
    except ValueError:
        print('Value error.')
    print('Continuing...')

If we execute this function, we get the following result:

>>> testException()
!
No errors so far.
Value error.
Continuing…

The first print() call in the try block will not raise an exception, but trying to convert myStr[12] to an integer will generate a ValueError exception.

You can also add a finally block to your code. The finally block is inserted after the try block and is executed whether or not an exception is generated (and whether or not said exception is handled). For example, we might have the following function:

def testException():
    myStr = 'Hello, world!'
    try:
        print(myStr[12])
        print('No errors so far.')
        b.append(myStr[12])
    except IndexError:
        print('Index error.')
    except ValueError:
        print('Value error.')
    finally:
        print('Finally block reached.')
    print('Continuing...')

Here, the line with the b.append() call will generate an unhandled exception because b was not previously defined as a list. Thus, it will not reach the last line of the program, but it will execute what is in the finally block:

!
No errors so far.
Finally block reached.
Traceback (most recent call last):
File “N:\Users\David\workspace\exception\exception.py”, line 16, in
testException()
File “N:\Users\David\workspace\exception\exception.py”, line 7, in testException
b.append(myStr[12])
NameError: global name ‘b’ is not defined

Exception handling in Python is not unlike try/catch blocks in C++ and try/catch/finally blocks in Java. It provides a useful alternative to the old C approach, which was to propagate error codes:

int functionCall()
{
	if (doSomething() == ERROR_CODE)
		return ERROR_CODE;
}

void main()
{
	if (functionCall() == ERROR_CODE)
		badResult();
	else
		goodResult();
}

In this case, a lot of our code is devoted to error handling. If we had a try/except block, however, we could just wrap the code we think might cause an error in a try block.

We are not quite done examining exception handling in Python, but hopefully this will serve as a good introduction.

External Links:

Handling Exceptions at Python Wiki

Built-in Exceptions at docs.python.org