PEP: 409 Title: Suppressing exception context Version: $Revision$
Last-Modified: $Date$ Author: Ethan Furman <ethan@stoneleaf.us> Status:
Final Type: Standards Track Content-Type: text/x-rst Created:
26-Jan-2012 Python-Version: 3.3 Post-History: 30-Aug-2002, 01-Feb-2012,
03-Feb-2012 Superseded-By: 415 Resolution:
https://mail.python.org/pipermail/python-dev/2012-February/116136.html

Abstract

One of the open issues from PEP 3134 is suppressing context: currently
there is no way to do it. This PEP proposes one.

Rationale

There are two basic ways to generate exceptions:

1)  Python does it (buggy code, missing resources, ending loops, etc.)
2)  manually (with a raise statement)

When writing libraries, or even just custom classes, it can become
necessary to raise exceptions; moreover it can be useful, even
necessary, to change from one exception to another. To take an example
from my dbf module:

    try:
        value = int(value)
    except Exception:
        raise DbfError(...)

Whatever the original exception was (ValueError, TypeError, or something
else) is irrelevant. The exception from this point on is a DbfError, and
the original exception is of no value. However, if this exception is
printed, we would currently see both.

Alternatives

Several possibilities have been put forth:

-   raise as NewException()

    Reuses the as keyword; can be confusing since we are not really
    reraising the originating exception

-   raise NewException() from None

    Follows existing syntax of explicitly declaring the originating
    exception

-   exc = NewException(); exc.__context__ = None; raise exc

    Very verbose way of the previous method

-   raise NewException.no_context(...)

    Make context suppression a class method.

All of the above options will require changes to the core.

Proposal

I propose going with the second option:

    raise NewException from None

It has the advantage of using the existing pattern of explicitly setting
the cause:

    raise KeyError() from NameError()

but because the cause is None the previous context is not displayed by
the default exception printing routines.

Implementation Discussion

Note: after acceptance of this PEP, a cleaner implementation mechanism
was proposed and accepted in PEP 415. Refer to that PEP for more details
on the implementation actually used in Python 3.3.

Currently, None is the default for both __context__ and __cause__. In
order to support raise ... from None (which would set __cause__ to None)
we need a different default value for __cause__. Several ideas were put
forth on how to implement this at the language level:

-   Overwrite the previous exception information (side-stepping the
    issue and leaving __cause__ at None).

    Rejected as this can seriously hinder debugging due to poor error
    messages.

-   Use one of the boolean values in __cause__: False would be the
    default value, and would be replaced when from ... was used with the
    explicitly chained exception or None.

    Rejected as this encourages the use of two different objects types
    for __cause__ with one of them (boolean) not allowed to have the
    full range of possible values (True would never be used).

-   Create a special exception class, __NoException__.

    Rejected as possibly confusing, possibly being mistakenly raised by
    users, and not being a truly unique value as None, True, and False
    are.

-   Use Ellipsis as the default value (the ... singleton).

    Accepted.

    Ellipses are commonly used in English as place holders when words
    are omitted. This works in our favor here as a signal that __cause__
    is omitted, so look in __context__ for more details.

    Ellipsis is not an exception, so cannot be raised.

    There is only one Ellipsis, so no unused values.

    Error information is not thrown away, so custom code can trace the
    entire exception chain even if the default code does not.

Language Details

To support raise Exception from None, __context__ will stay as it is,
but __cause__ will start out as Ellipsis and will change to None when
the raise Exception from None method is used.

  form                                   __context__          __cause__
  -------------------------------------- -------------------- -------------------------------------
  raise                                  None                 Ellipsis
  reraise                                previous exception   Ellipsis
  reraise from None | ChainedException   previous exception   None | explicitly chained exception

The default exception printing routine will then:

-   If __cause__ is Ellipsis the __context__ (if any) will be printed.
-   If __cause__ is None the __context__ will not be printed.
-   if __cause__ is anything else, __cause__ will be printed.

In both of the latter cases the exception chain will stop being
followed.

Because the default value for __cause__ is now Ellipsis and
raise Exception from Cause is simply syntactic sugar for:

    _exc = NewException()
    _exc.__cause__ = Cause()
    raise _exc

Ellipsis, as well as None, is now allowed as a cause:

    raise Exception from Ellipsis

Patches

There is a patch for CPython implementing this attached to Issue 6210.

References

Discussion and refinements in this thread on python-dev.

Copyright

This document has been placed in the public domain.



  Local Variables: mode: indented-text indent-tabs-mode: nil
  sentence-end-double-space: t fill-column: 70 coding: utf-8 End: