This PEP started its life asPEP 344. Since it is now targeted for Python3000, it has been moved into the 3xxx space.
This PEP proposes three standard attributes on exception instances: the__context__ attribute for implicitly chained exceptions, the__cause__attribute for explicitly chained exceptions, and the__traceback__attribute for the traceback. A newraise...from statement sets the__cause__ attribute.
During the handling of one exception (exception A), it is possible that anotherexception (exception B) may occur. In today’s Python (version 2.4), if thishappens, exception B is propagated outward and exception A is lost. In orderto 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-raisean exception, either to provide extra information or to translate an exceptionto another type. The__cause__ attribute provides an explicit way torecord the direct cause of an exception.
In today’s Python implementation, exceptions are composed of three parts: thetype, the value, and the traceback. Thesys module, exposes the currentexception in three parallel variables,exc_type,exc_value, andexc_traceback, thesys.exc_info() function returns a tuple of thesethree parts, and theraise statement has a three-argument form acceptingthese three parts. Manipulating exceptions often requires passing these threethings in parallel, which can be tedious and error-prone. Additionally, theexcept statement can only provide access to the value, not the traceback.Adding the__traceback__ attribute to exception values makes all theexception information accessible from a single place.
Raymond Hettinger[1] raised the issue of masked exceptions on Python-Dev inJanuary 2003 and proposed aPyErr_FormatAppend() function that C modulescould use to augment the currently active exception with more information.Brett Cannon[2] brought up chained exceptions again in June 2003, promptinga long discussion.
Greg Ewing[3] identified the case of an exception occurring in afinallyblock during unwinding triggered by an original exception, as distinct fromthe case of an exception occurring in anexcept block that is handling theoriginal exception.
Greg Ewing[4] and Guido van Rossum[5], and probably others, havepreviously mentioned adding a traceback attribute to Exception instances.This is noted inPEP 3000.
This PEP was motivated by yet another recent Python-Dev reposting of the sameideas[6][7].
The Python-Dev discussions revealed interest in exception chaining for twoquite different purposes. To handle the unexpected raising of a secondaryexception, the exception must be retained implicitly. To support intentionaltranslation of an exception, there must be a way to chain exceptionsexplicitly. This PEP addresses both.
Several attribute names for chained exceptions have been suggested onPython-Dev[2], includingcause,antecedent,reason,original,chain,chainedexc,exc_chain,excprev,previous, andprecursor. For an explicitly chained exception, this PEP suggests__cause__ because of its specific meaning. For an implicitly chainedexception, this PEP proposes the name__context__ because the intendedmeaning is more specific than temporal precedence but less specific thancausation: an exception occurs in the context of handling another exception.
This PEP suggests names with leading and trailing double-underscores for thesethree attributes because they are set by the Python VM. Only in very specialcases should they be set by normal assignment.
This PEP handles exceptions that occur duringexcept blocks andfinallyblocks in the same way. Reading the traceback makes it clear where theexceptions occurred, so additional mechanisms for distinguishing the two caseswould only add unnecessary complexity.
This PEP proposes that the outermost exception object (the one exposed formatching byexcept clauses) be the most recently raised exception forcompatibility with current behaviour.
This PEP proposes that tracebacks display the outermost exception last, becausethis would be consistent with the chronological order of tracebacks (fromoldest to most recent frame) and because the actual thrown exception is easierto find on the last line.
To keep things simpler, the C API calls for setting an exception will notautomatically set the exception’s__context__. Guido van Rossum hasexpressed concerns with making such changes[8].
As for other languages, Java and Ruby both discard the original exception whenanother exception occurs in acatch/rescue orfinally/ensureclause. Perl 5 lacks built-in structured exception handling. For Perl 6, RFCnumber 88[9] proposes an exception mechanism that implicitly retains chainedexceptions in an array named@@. In that RFC, the most recently raisedexception is exposed for matching, as in this PEP; also, arbitrary expressions(possibly involving@@) can be evaluated for exception matching.
Exceptions in C# contain a read-onlyInnerException property that may pointto another exception. Its documentation[10] says that “When an exception Xis thrown as a direct result of a previous exception Y, theInnerExceptionproperty of X should contain a reference to Y.” This property is not set bythe VM automatically; rather, all exception constructors take an optionalinnerException argument to set it explicitly. The__cause__ attributefulfills the same purpose asInnerException, but this PEP proposes a newform ofraise rather than extending the constructors of all exceptions. C#also provides aGetBaseException method that jumps directly to the end oftheInnerException chain; this PEP proposes no analog.
The reason all three of these attributes are presented together in one proposalis that the__traceback__ attribute provides convenient access to thetraceback on chained exceptions.
Here is an example to illustrate the__context__ attribute:
defcompute(a,b):try:a/bexceptException,exc:log(exc)deflog(exc):file=open('logfile.txt')# oops, forgot the 'w'print>>file,excfile.close()
Callingcompute(0,0) causes aZeroDivisionError. Thecompute()function catches this exception and callslog(exc), but thelog()function also raises an exception when it tries to write to a file that wasn’topened for writing.
In today’s Python, the caller ofcompute() gets thrown anIOError. TheZeroDivisionError is lost. With the proposed change, the instance ofIOError has an additional__context__ attribute that retains theZeroDivisionError.
The following more elaborate example demonstrates the handling of a mixture offinally andexcept clauses:
defmain(filename):file=open(filename)# oops, forgot the 'w'try:try:compute()exceptException,exc:log(file,exc)finally:file.clos()# oops, misspelled 'close'defcompute():1/0deflog(file,exc):try:print>>file,exc# oops, file is not writableexcept:display(exc)defdisplay(exc):printex# oops, misspelled 'exc'
Callingmain() with the name of an existing file will trigger fourexceptions. The ultimate result will be anAttributeError due to themisspelling ofclos, whose__context__ points to aNameError dueto the misspelling ofex, whose__context__ points to anIOErrordue to the file being read-only, whose__context__ points to aZeroDivisionError, whose__context__ attribute isNone.
The proposed semantics are as follows:
None.__context__ attribute, the interpreter sets it equal to thethread’s exception context.except block by reaching the end orexecuting areturn,yield,continue, orbreak statement, thethread’s exception context is set toNone.The__cause__ attribute on exception objects is always initialized toNone. It is set by a new form of theraise statement:
raiseEXCEPTIONfromCAUSE
which is equivalent to:
exc=EXCEPTIONexc.__cause__=CAUSEraiseexc
In the following example, a database provides implementations for a fewdifferent kinds of storage, with file storage as one kind. The databasedesigner wants errors to propagate asDatabaseError objects so that theclient doesn’t have to be aware of the storage-specific details, but doesn’twant to lose the underlying error information.
classDatabaseError(Exception):passclassFileDatabase(Database):def__init__(self,filename):try:self.file=open(filename)exceptIOError,exc:raiseDatabaseError('failed to open')fromexc
If the call toopen() raises an exception, the problem will be reported asaDatabaseError, with a__cause__ attribute that reveals theIOError as the original cause.
The following example illustrates the__traceback__ attribute.
defdo_logged(file,work):try:work()exceptException,exc:write_exception(file,exc)raiseexcfromtracebackimportformat_tbdefwrite_exception(file,exc):...type=exc.__class__message=str(exc)lines=format_tb(exc.__traceback__)file.write(...type...message...lines...)...
In today’s Python, thedo_logged() function would have to extract thetraceback fromsys.exc_traceback orsys.exc_info()[2] and pass boththe value and the traceback towrite_exception(). With the proposedchange,write_exception() simply gets one argument and obtains theexception using the__traceback__ attribute.
The proposed semantics are as follows:
__traceback__ attribute, the interpreter sets it to the newlycaught traceback.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 keepingwith the chronological order of tracebacks, the most recently raised exceptionis displayed last; that is, the display begins with the description of theinnermost exception and backs up the chain to the outermost exception. Thetracebacks are formatted as usual, with one of the lines:
Theaboveexceptionwasthedirectcauseofthefollowingexception:
or
Duringhandlingoftheaboveexception,anotherexceptionoccurred:
between tracebacks, depending whether they are linked by__cause__ or__context__ respectively. Here is a sketch of the procedure:
defprint_chain(exc):ifexc.__cause__:print_chain(exc.__cause__)print'\nThe above exception was the direct cause...'elifexc.__context__:print_chain(exc.__context__)print'\nDuring handling of the above exception, ...'print_exc(exc)
In thetraceback module, theformat_exception,print_exception,print_exc, andprint_last functions will be updated to accept anoptionalchain argument,True by default. When this argument isTrue, these functions will format or display the entire chain of exceptionsas just described. When it isFalse, these functions will format ordisplay only the outermost exception.
Thecgitb module should also be updated to display the entire chain ofexceptions.
ThePyErr_Set* calls for setting exceptions will not set the__context__ attribute on exceptions.PyErr_NormalizeException willalways set thetraceback attribute to itstb argument and the__context__ and__cause__ attributes toNone.
A new API function,PyErr_SetContext(context), will help C programmersprovide chained exception information. This function will first normalize thecurrent exception so it is an instance, then set its__context__ attribute.A similar API function,PyErr_SetCause(cause), will set the__cause__attribute.
Chained exceptions expose the type of the most recent exception, so they willstill match the sameexcept clauses as they do now.
The proposed changes should not break any code unless it sets or usesattributes named__context__,__cause__, or__traceback__ onexception instances. As of 2005-05-12, the Python standard library contains nomention of such attributes.
Walter Dörwald[11] expressed a desire to attach extra information to anexception during its upward propagation without changing its type. This couldbe a useful feature, but it is not addressed by this PEP. It could conceivablybe addressed by a separate PEP establishing conventions for other informationalattributes on exceptions.
As written, this PEP makes it impossible to suppress__context__, sincesettingexc.__context__ toNone in anexcept orfinally clausewill only result in it being set again whenexc is raised.
To improve encapsulation, library implementors may want to wrap allimplementation-level exceptions with an application-level exception. One couldtry to wrap exceptions by writing this:
try:...implementationmayraiseanexception...except:importsysraiseApplicationErrorfromsys.exc_value
or this:
try:...implementationmayraiseanexception...exceptException,exc:raiseApplicationErrorfromexc
but both are somewhat flawed. It would be nice to be able to name the currentexception in a catch-allexcept clause, but that isn’t addressed here.Such a feature would allow something like this:
try:...implementationmayraiseanexception...except*,exc:raiseApplicationErrorfromexc
The exception context is lost when ayield statement is executed; resumingthe frame after theyield does not restore the context. Addressing thisproblem is out of the scope of this PEP; it is not a new problem, asdemonstrated by the following example:
>>>defgen():...try:...1/0...except:...yield3...raise...>>>g=gen()>>>g.next()3>>>g.next()TypeError:exceptionsmustbeclasses,instances,orstrings(deprecated),notNoneType
The strongest objection to this proposal has been that it creates cyclesbetween exceptions and stack frames[12]. Collection of cyclic garbage (andtherefore resource release) can be greatly delayed.
>>>try:>>>1/0>>>exceptException,err:>>>pass
will introduce a cycle from err -> traceback -> stack frame -> err, keeping alllocals in the same scope alive until the next GC happens.
Today, these locals would go out of scope. There is lots of code which assumesthat “local” resources – particularly open files – will be closed quickly.If closure has to wait for the next GC, a program (which runs fine today) mayrun out of file handles.
Making the__traceback__ attribute a weak reference would avoid theproblems with cyclic garbage. Unfortunately, it would make saving theException for later (asunittest does) more awkward, and it would notallow as much cleanup of thesys module.
A possible alternate solution, suggested by Adam Olsen, would be to insteadturn the reference from the stack frame to theerr variable into a weakreference when the variable goes out of scope[13].
These changes are consistent with the appearance of exceptions as a singleobject rather than a triple at the interpreter level.
type,value,traceback) arguments to__exit__ with a single exception argument.sys.exc_type,sys.exc_value,sys.exc_traceback, andsys.exc_info() in favour of a single member,sys.exception.sys.last_type,sys.last_value, andsys.last_tracebackin favour of a single member,sys.last_exception.raise statement in favour of theone-argument form.cgitb.html() to accept a single value as its first argument as analternative to a(type,value,traceback) tuple.These changes might be worth considering for Python 3000.
sys.exc_type,sys.exc_value,sys.exc_traceback, andsys.exc_info().sys.last_type,sys.last_value, andsys.last_traceback.sys.excepthook with a one-argument API, andchanging thecgitb module to match.raise statement.traceback.print_exception to accept anexception argumentinstead of thetype,value, andtraceback arguments.The__traceback__ and__cause__ attributes and the new raise syntaxwere implemented in revision 57783[14].
Brett Cannon, Greg Ewing, Guido van Rossum, Jeremy Hylton, Phillip J. Eby,Raymond Hettinger, Walter Dörwald, and others.
PyErr_Set*https://mail.python.org/pipermail/python-dev/2003-June/036180.htmlThis document has been placed in the public domain.
Source:https://github.com/python/peps/blob/main/peps/pep-3134.rst
Last modified:2025-02-01 08:59:27 GMT