In 2005, PEP 3134, Exception Chaining and Embedded Tracebacks introduced exception chaining:
- implicit chaining with explicit
raise EXCEPTION
or implicit raise (__context__
attribute);
- explicit chaining with explicit
raise EXCEPTION from CAUSE
(__cause__
attribute).
Motivation
During the handling of one exception (exception A), it is possible that another exception (exception B) may occur. In today’s Python (version 2.4), if this happens, exception B is propagated outward and exception A is lost. In order to debug the problem, it is useful to know about both exceptions. The __context__
attribute retains this information automatically.
Sometimes it can be useful for an exception handler to intentionally re-raise an exception, either to provide extra information or to translate an exception to another type. The __cause__
attribute provides an explicit way to record the direct cause of an exception.
[…]
Implicit Exception Chaining
Here is an example to illustrate the __context__
attribute:
def compute(a, b):
try:
a/b
except Exception, exc:
log(exc)
def log(exc):
file = open('logfile.txt') # oops, forgot the 'w'
print >>file, exc
file.close()
Calling compute(0, 0)
causes a ZeroDivisionError
. The compute()
function catches this exception and calls log(exc)
, but the log()
function also raises an exception when it tries to write to a file that wasn’t opened for writing.
In today’s Python, the caller of compute()
gets thrown an IOError
. The ZeroDivisionError
is lost. With the proposed change, the instance of IOError
has an additional __context__
attribute that retains the ZeroDivisionError
.
[…]
Explicit Exception Chaining
The __cause__
attribute on exception objects is always initialized to None
. It is set by a new form of the raise
statement:
raise EXCEPTION from CAUSE
which is equivalent to:
exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc
In the following example, a database provides implementations for a few different kinds of storage, with file storage as one kind. The database designer wants errors to propagate as DatabaseError
objects so that the client doesn’t have to be aware of the storage-specific details, but doesn’t want to lose the underlying error information.
class DatabaseError(Exception):
pass
class FileDatabase(Database):
def __init__(self, filename):
try:
self.file = open(filename)
except IOError, exc:
raise DatabaseError('failed to open') from exc
If the call to open()
raises an exception, the problem will be reported as a DatabaseError
, with a __cause__
attribute that reveals the IOError
as the original cause.
Enhanced Reporting
The default exception handler will be modified to report chained exceptions. The chain of exceptions is traversed by following the __cause__
and __context__
attributes, with __cause__
taking priority. In keeping with the chronological order of tracebacks, the most recently raised exception is displayed last; that is, the display begins with the description of the innermost exception and backs up the chain to the outermost exception. The tracebacks are formatted as usual, with one of the lines:
The above exception was the direct cause of the following exception:
or
During handling of the above exception, another exception occurred:
between tracebacks, depending whether they are linked by __cause__
or __context__
respectively. Here is a sketch of the procedure:
def print_chain(exc):
if exc.__cause__:
print_chain(exc.__cause__)
print '\nThe above exception was the direct cause...'
elif exc.__context__:
print_chain(exc.__context__)
print '\nDuring handling of the above exception, ...'
print_exc(exc)
[…]
In 2012, PEP 415, Implement Context Suppression with Exception Attributes introduced exception context suppression with explicit raise EXCEPTION from None
(__suppress_context__
attribute).
Proposal
A new attribute on BaseException
, __suppress_context__
, will be introduced. Whenever __cause__
is set, __suppress_context__
will be set to True
. In particular, raise exc from cause
syntax will set exc.__suppress_context__
to True
. Exception printing code will check for that attribute to determine whether context and cause will be printed. __cause__
will return to its original purpose and values.
There is precedence for __suppress_context__
with the print_line_and_file
exception attribute.
To summarize, raise exc from cause
will be equivalent to:
exc.__cause__ = cause
raise exc
where exc.__cause__ = cause
implicitly sets exc.__suppress_context__
.
So in PEP 415, the sketch of the procedure given in PEP 3134 for the default exception handler (its job is to report exceptions) becomes the following:
def print_chain(exc):
if exc.__cause__:
print_chain(exc.__cause__)
print '\nThe above exception was the direct cause...'
elif exc.__context__ and not exc.__suppress_context__:
print_chain(exc.__context__)
print '\nDuring handling of the above exception, ...'
print_exc(exc)
raise IndexError from None
, say. – Boilermakerraise IndexError from False
raises aTypeError
, not anIndexError
. Made my day. – Huddleston