360 lines
13 KiB
Plaintext
360 lines
13 KiB
Plaintext
PEP: 344
|
||
Title: Exception Chaining and the Traceback Attribute
|
||
Version: $Revision$
|
||
Last-Modified: $Date$
|
||
Author: Ka-Ping Yee
|
||
Status: Active
|
||
Type: Standards Track
|
||
Content-Type: text/plain
|
||
Created: 12-May-2005
|
||
Python-Version: 2.5
|
||
|
||
|
||
Abstract
|
||
|
||
This PEP proposes two standard attributes on exception instances:
|
||
the 'context' attribute for chained exceptions, and the 'traceback'
|
||
attribute for the traceback.
|
||
|
||
|
||
Motivation
|
||
|
||
Sometimes, during the handling of one exception (exception A),
|
||
another exception (exception B) can occur. In today's Python
|
||
(version 2.4), if this happens, exception B is propagated outward
|
||
and exception A is lost. But in order to debug the problem, it is
|
||
useful to know about both exceptions. The 'context' attribute
|
||
retains this information.
|
||
|
||
In today's Python implementation, exceptions are composed of three
|
||
parts: the type, the value, and the traceback. The 'sys' module,
|
||
exposes the current exception in three parallel variables, exc_type,
|
||
exc_value, and exc_traceback, the sys.exc_info() function returns a
|
||
tuple of these three parts, and the 'raise' statement has a
|
||
three-argument form accepting these three parts. Manipulating
|
||
exceptions often requires passing these three things in parallel,
|
||
which can be tedious and error-prone. Additionally, the 'except'
|
||
statement can only provide access to the value, not the traceback.
|
||
Adding the 'traceback' attribute to exception values makes all the
|
||
exception information accessible from a single place.
|
||
|
||
The reason both of these attributes are presented together in one
|
||
proposal is that the 'traceback' attribute provides convenient
|
||
access to the traceback on chained exceptions.
|
||
|
||
|
||
History
|
||
|
||
Raymond Hettinger [1] raised the issue of masked exceptions on
|
||
Python-Dev in January 2003 and proposed a PyErr_FormatAppend()
|
||
function that C modules could use to augment the currently active
|
||
exception with more information.
|
||
|
||
Brett Cannon [2] brought up chained exceptions again in June 2003
|
||
and a long discussion followed. Other suggested attribute names
|
||
include 'cause', 'antecedent', 'reason', 'original', 'chain',
|
||
'chainedexc', 'exc_chain', 'excprev', 'previous', and 'precursor'.
|
||
This PEP suggests 'context' because the intended meaning is more
|
||
specific than temporal precedence and less specific than causation:
|
||
an exception occurs in the *context* of handling another exception.
|
||
|
||
Greg Ewing [3] identified the case of an exception occuring in a
|
||
'finally' block during unwinding triggered by an original exception,
|
||
as distinct from the case of an exception occuring in an 'except'
|
||
block that is handling the original exception. This PEP handles
|
||
both situations in the same way; it is assumed to be unnecessary to
|
||
add mechanisms for distinguishing them since the programmer can tell
|
||
them apart by reading the traceback.
|
||
|
||
Greg Ewing [4] and Guido van Rossum [5], and probably others, have
|
||
previously mentioned adding the traceback attribute to Exception
|
||
instances. This is noted in PEP 3000.
|
||
|
||
This PEP was motivated by yet another recent Python-Dev reposting
|
||
of the same ideas [6] [7].
|
||
|
||
|
||
*** Add rationale for: choice of name, handling both finally/except,
|
||
handling finally/except the same, displaying outermost last.
|
||
|
||
*** Compare to Java: exceptions are lost in catch or finally clauses.
|
||
|
||
*** Compare to Ruby: exceptions are lost just like Java.
|
||
|
||
*** Compare to C#:
|
||
|
||
*** Compare to E:
|
||
|
||
*** COmpare to Perl: RFC 88 a mess.
|
||
|
||
*** Note http://pclt.cis.yale.edu/pclt/exceptions.htm
|
||
|
||
|
||
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.
|
||
|
||
The following more elaborate example demonstrates the handling of a
|
||
mix of 'finally' and 'except' clauses:
|
||
|
||
def main(filename):
|
||
file = open(filename) # oops, forgot the 'w'
|
||
try:
|
||
try:
|
||
compute()
|
||
except Exception, exc:
|
||
log(file, exc)
|
||
finally:
|
||
file.clos() # oops, misspelled 'close'
|
||
|
||
def compute():
|
||
1/0
|
||
|
||
def log(file, exc):
|
||
try:
|
||
print >>file, exc # oops, file is not writable
|
||
except:
|
||
display(exc)
|
||
|
||
def display(exc):
|
||
print ex # oops, misspelled 'exc'
|
||
|
||
Calling main() with the name of an existing file will trigger four
|
||
exceptions. The ultimate result will be an AttributeError due to
|
||
the misspelling of 'clos', which has a context attribute pointing to
|
||
a NameError due to the misspelling of 'ex', which has a context
|
||
attribute pointing to an IOError due to the file being read-only,
|
||
which has a context attribute pointing to a ZeroDivisionError, which
|
||
has a context attribute of None.
|
||
|
||
The proposed semantics are as follows:
|
||
|
||
1. Each thread has an exception context initially set to None.
|
||
|
||
2. Whenever an exception is raised, if the exception instance does
|
||
not already have a 'context' attribute, the interpreter sets it
|
||
equal to the thread's exception context.
|
||
|
||
3. Immediately after an exception is raised, the thread's exception
|
||
context is set to the exception.
|
||
|
||
4. Whenever the interpreter exits an 'except' block by reaching the
|
||
end or executing a 'return', 'yield', 'continue', or 'break'
|
||
statement, the thread's exception context is set to None.
|
||
|
||
|
||
Traceback Attribute
|
||
|
||
The following example illustrates the 'traceback' attribute.
|
||
|
||
def do_logged(file, work):
|
||
try:
|
||
work()
|
||
except Exception, exc:
|
||
write_exception(file, exc)
|
||
raise exc
|
||
|
||
from traceback import format_tb
|
||
|
||
def write_exception(file, exc):
|
||
...
|
||
type = exc.__class__
|
||
message = str(exc)
|
||
lines = format_tb(exc.traceback)
|
||
file.write(... type ... message ... lines ...)
|
||
...
|
||
|
||
In today's Python, the do_logged() function would have to extract
|
||
the traceback from sys.exc_traceback or sys.exc_info()[2] and pass
|
||
both the value and the traceback to write_exception(). With the
|
||
proposed change, write_exception() simply gets one argument and
|
||
obtains the exception using the 'traceback' attribute.
|
||
|
||
The proposed semantics are as follows:
|
||
|
||
1. Whenever an exception is raised, if the exception instance does
|
||
not already have a 'traceback' attribute, the interpreter sets
|
||
it to the newly raised traceback.
|
||
|
||
|
||
Enhanced Reporting
|
||
|
||
The default exception handler will be modified to report chained
|
||
exceptions. In keeping with the chronological order of tracebacks,
|
||
the most recently raised exception is displayed last. 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 the following line between tracebacks:
|
||
|
||
During handling of the above exception, another exception occurred:
|
||
|
||
In the 'traceback' module, the format_exception, print_exception,
|
||
print_exc, and print_last functions will be updated to accept an
|
||
optional 'context' argument, True by default. When this argument is
|
||
True, these functions will format or display the entire chain of
|
||
exceptions as just described. When it is False, these functions
|
||
will format or display only the outermost exception.
|
||
|
||
The 'cgitb' module will be updated to display the entire chain of
|
||
exceptions.
|
||
|
||
|
||
C API
|
||
|
||
To keep things simpler, the PyErr_Set* calls for setting exceptions
|
||
will not set the 'context' attribute on exceptions. Guido van Rossum
|
||
has expressed qualms with making such changes to PyErr_Set* [8].
|
||
|
||
PyErr_NormalizeException will always set the 'traceback' attribute
|
||
to its 'tb' argument and the 'context' attribute to None.
|
||
|
||
A new API function, PyErr_SetContext(context), will help C
|
||
programmers provide chained exception information. This function
|
||
will first normalize the current exception so it is an instance,
|
||
then set its 'context' attribute.
|
||
|
||
|
||
Compatibility
|
||
|
||
Chained exceptions expose their outermost type so that they will
|
||
continue to match the same 'except' clauses as they do now.
|
||
|
||
The proposed changes should not break any code except for code
|
||
that currently sets and relies on the values of attributes named
|
||
'context' or 'traceback' on exceptions.
|
||
|
||
As of 2005-05-12, the Python standard library contains no mention
|
||
of such attributes.
|
||
|
||
|
||
Open Issues
|
||
|
||
Walter D<>rwald [9] expressed a desire to attach extra information
|
||
to an exception during its upward propagation, without changing its
|
||
type. This could be a useful feature, but it is not addressed by
|
||
this PEP. It could conceivably be addressed by a separate PEP
|
||
establishing conventions for other informational attributes on
|
||
exceptions.
|
||
|
||
It is not clear whether the 'context' feature proposed here would
|
||
be sufficient to cover all the use cases that Raymond Hettinger [1]
|
||
originally had in mind.
|
||
|
||
The exception context is lost when a 'yield' statement is executed;
|
||
resuming the frame after the 'yield' does not restore the context.
|
||
This is not a new problem, as demonstrated by the following example:
|
||
|
||
>>> def gen():
|
||
... try:
|
||
... 1/0
|
||
... except:
|
||
... yield 3
|
||
... raise
|
||
...
|
||
>>> g = gen()
|
||
>>> g.next()
|
||
3
|
||
>>> g.next()
|
||
TypeError: exceptions must be classes, instances, or strings
|
||
(deprecated), not NoneType
|
||
|
||
For now, addressing this problem is out of the scope of this PEP.
|
||
|
||
|
||
Possible Future Compatible Changes
|
||
|
||
These changes are consistent with the appearance of exceptions as
|
||
a single object rather than a triple at the interpreter level.
|
||
|
||
- Deprecating sys.exc_type, sys.exc_value, sys.exc_traceback, and
|
||
sys.exc_info() in favour of a single member, sys.exception.
|
||
|
||
- Deprecating sys.last_type, sys.last_value, and sys.last_traceback
|
||
in favour of a single member, sys.last_exception.
|
||
|
||
- Deprecating the three-argument form of the 'raise' statement in
|
||
favour of the one-argument form.
|
||
|
||
- Upgrading cgitb.html() to accept a single value as its first
|
||
argument as an alternative to a (type, value, traceback) tuple.
|
||
|
||
|
||
Possible Future Incompatible Changes
|
||
|
||
These changes might be worth considering for Python 3000.
|
||
|
||
- Removing sys.exc_type, sys.exc_value, sys.exc_traceback, and
|
||
sys.exc_info().
|
||
|
||
- Removing sys.last_type, sys.last_value, and sys.last_traceback.
|
||
|
||
- Replacing the three-argument sys.excepthook with a one-argument
|
||
API, and changing the 'cgitb' module to match.
|
||
|
||
- Removing the three-argument form of the 'raise' statement.
|
||
|
||
- Upgrading traceback.print_exception to accept an 'exception'
|
||
argument instead of the type, value, and traceback arguments.
|
||
|
||
|
||
Acknowledgements
|
||
|
||
Brett Cannon, Greg Ewing, Guido van Rossum, Jeremy Hylton, Phillip
|
||
J. Eby, Raymond Hettinger, Walter D<>rwald, and others.
|
||
|
||
|
||
References
|
||
|
||
[1] Raymond Hettinger, "Idea for avoiding exception masking"
|
||
http://mail.python.org/pipermail/python-dev/2003-January/032492.html
|
||
|
||
[2] Brett Cannon explains chained exceptions
|
||
http://mail.python.org/pipermail/python-dev/2003-June/036063.html
|
||
|
||
[3] Greg Ewing points out masking caused by exceptions during finally
|
||
http://mail.python.org/pipermail/python-dev/2003-June/036290.html
|
||
|
||
[4] Greg Ewing suggests storing the traceback in the exception object
|
||
http://mail.python.org/pipermail/python-dev/2003-June/036092.html
|
||
|
||
[5] Guido van Rossum mentions exceptions having a traceback attribute
|
||
http://mail.python.org/pipermail/python-dev/2005-April/053060.html
|
||
|
||
[6] Ka-Ping Yee, "Tidier Exceptions"
|
||
http://mail.python.org/pipermail/python-dev/2005-May/053671.html
|
||
|
||
[7] Ka-Ping Yee, "Chained Exceptions"
|
||
http://mail.python.org/pipermail/python-dev/2005-May/053672.html
|
||
|
||
[8] Guido van Rossum discusses automatic chaining in PyErr_Set*
|
||
http://mail.python.org/pipermail/python-dev/2003-June/036180.html
|
||
|
||
[9] Walter D<>rwald suggests wrapping exceptions to add details
|
||
http://mail.python.org/pipermail/python-dev/2003-June/036148.html
|
||
|
||
|
||
Copyright
|
||
|
||
This document has been placed in the public domain.
|