PEP: 317 Title: Eliminate Implicit Exception Instantiation Version:
$Revision$ Last-Modified: $Date$ Author: Steven Taschuk
<staschuk@telusplanet.net> Status: Rejected Type: Standards Track
Content-Type: text/x-rst Created: 06-May-2003 Python-Version: 2.4
Post-History: 09-Jun-2003

Abstract

  "For clarity in new code, the form raise class(argument, ...) is
  recommended (i.e. make an explicit call to the constructor)."

  -- Guido van Rossum, in 1997[1]

This PEP proposes the formal deprecation and eventual elimination of
forms of the raise statement which implicitly instantiate an exception.
For example, statements such as :

    raise HullBreachError
    raise KitchenError, 'all out of baked beans'

must under this proposal be replaced with their synonyms :

    raise HullBreachError()
    raise KitchenError('all out of baked beans')

Note that these latter statements are already legal, and that this PEP
does not change their meaning.

Eliminating these forms of raise makes it impossible to use string
exceptions; accordingly, this PEP also proposes the formal deprecation
and eventual elimination of string exceptions.

Adoption of this proposal breaks backwards compatibility. Under the
proposed implementation schedule, Python 2.4 will introduce warnings
about uses of raise which will eventually become incorrect, and Python
3.0 will eliminate them entirely. (It is assumed that this transition
period -- 2.4 to 3.0 -- will be at least one year long, to comply with
the guidelines of PEP 5.)

Motivation

String Exceptions

It is assumed that removing string exceptions will be uncontroversial,
since it has been intended since at least Python 1.5, when the standard
exception types were changed to classes[2].

For the record: string exceptions should be removed because the presence
of two kinds of exception complicates the language without any
compensation. Instance exceptions are superior because, for example,

-   the class-instance relationship more naturally expresses the
    relationship between the exception type and value,
-   they can be organized naturally using superclass-subclass
    relationships, and
-   they can encapsulate error-reporting behaviour (for example).

Implicit Instantiation

Guido's 1997 essay[3] on changing the standard exceptions into classes
makes clear why raise can instantiate implicitly:

  "The raise statement has been extended to allow raising a class
  exception without explicit instantiation. The following forms, called
  the "compatibility forms" of the raise statement [...] The motivation
  for introducing the compatibility forms was to allow backward
  compatibility with old code that raised a standard exception."

For example, it was desired that pre-1.5 code which used string
exception syntax such as :

    raise TypeError, 'not an int'

would work both on versions of Python in which TypeError was a string,
and on versions in which it was a class.

When no such consideration obtains -- that is, when the desired
exception type is not a string in any version of the software which the
code must support -- there is no good reason to instantiate implicitly,
and it is clearer not to. For example:

1.  In the code :

        try:
            raise MyError, raised
        except MyError, caught:
            pass

    the syntactic parallel between the raise and except statements
    strongly suggests that raised and caught refer to the same object.
    For string exceptions this actually is the case, but for instance
    exceptions it is not.

2.  When instantiation is implicit, it is not obvious when it occurs,
    for example, whether it occurs when the exception is raised or when
    it is caught. Since it actually happens at the raise, the code
    should say so.

    (Note that at the level of the C API, an exception can be "raised"
    and "caught" without being instantiated; this is used as an
    optimization by, for example, PyIter_Next. But in Python, no such
    optimization is or should be available.)

3.  An implicitly instantiating raise statement with no arguments, such
    as :

        raise MyError

    simply does not do what it says: it does not raise the named object.

4.  The equivalence of :

        raise MyError
        raise MyError()

    conflates classes and instances, creating a possible source of
    confusion for beginners. (Moreover, it is not clear that the
    interpreter could distinguish between a new-style class and an
    instance of such a class, so implicit instantiation may be an
    obstacle to any future plan to let exceptions be new-style objects.)

In short, implicit instantiation has no advantages other than backwards
compatibility, and so should be phased out along with what it exists to
ensure compatibility with, namely, string exceptions.

Specification

The syntax of raise_stmt[4] is to be changed from :

    raise_stmt ::= "raise" [expression ["," expression ["," expression]]]

to :

    raise_stmt ::= "raise" [expression ["," expression]]

If no expressions are present, the raise statement behaves as it does
presently: it re-raises the last exception that was active in the
current scope, and if no exception has been active in the current scope,
a TypeError is raised indicating that this is the problem.

Otherwise, the first expression is evaluated, producing the raised
object. Then the second expression is evaluated, if present, producing
the substituted traceback. If no second expression is present, the
substituted traceback is None.

The raised object must be an instance. The class of the instance is the
exception type, and the instance itself is the exception value. If the
raised object is not an instance -- for example, if it is a class or
string -- a TypeError is raised.

If the substituted traceback is not None, it must be a traceback object,
and it is substituted instead of the current location as the place where
the exception occurred. If it is neither a traceback object nor None, a
TypeError is raised.

Backwards Compatibility

Migration Plan

Future Statement

Under the PEP 236 future statement:

    from __future__ import raise_with_two_args

the syntax and semantics of the raise statement will be as described
above. This future feature is to appear in Python 2.4; its effect is to
become standard in Python 3.0.

As the examples below illustrate, this future statement is only needed
for code which uses the substituted traceback argument to raise; simple
exception raising does not require it.

Warnings

Three new warnings <230>, all of category DeprecationWarning, are to be
issued to point out uses of raise which will become incorrect under the
proposed changes.

The first warning is issued when a raise statement is executed in which
the first expression evaluates to a string. The message for this warning
is:

    raising strings will be impossible in the future

The second warning is issued when a raise statement is executed in which
the first expression evaluates to a class. The message for this warning
is:

    raising classes will be impossible in the future

The third warning is issued when a raise statement with three
expressions is compiled. (Not, note, when it is executed; this is
important because the SyntaxError which this warning presages will occur
at compile-time.) The message for this warning is:

    raising with three arguments will be impossible in the future

These warnings are to appear in Python 2.4, and disappear in Python 3.0,
when the conditions which cause them are simply errors.

Examples

Code Using Implicit Instantiation

Code such as :

    class MyError(Exception):
        pass

    raise MyError, 'spam'

will issue a warning when the raise statement is executed. The raise
statement should be changed to instantiate explicitly:

    raise MyError('spam')

Code Using String Exceptions

Code such as :

    MyError = 'spam'
    raise MyError, 'eggs'

will issue a warning when the raise statement is executed. The exception
type should be changed to a class:

    class MyError(Exception):
        pass

and, as in the previous example, the raise statement should be changed
to instantiate explicitly :

    raise MyError('eggs')

Code Supplying a Traceback Object

Code such as :

    raise MyError, 'spam', mytraceback

will issue a warning when compiled. The statement should be changed to :

    raise MyError('spam'), mytraceback

and the future statement :

    from __future__ import raise_with_two_args

should be added at the top of the module. Note that adding this future
statement also turns the other two warnings into errors, so the changes
described in the previous examples must also be applied.

The special case :

    raise sys.exc_type, sys.exc_info, sys.exc_traceback

(which is intended to re-raise a previous exception) should be changed
simply to :

    raise

A Failure of the Plan

It may occur that a raise statement which raises a string or implicitly
instantiates is not executed in production or testing during the
phase-in period for this PEP. In that case, it will not issue any
warnings, but will instead suddenly fail one day in Python 3.0 or a
subsequent version. (The failure is that the wrong exception gets
raised, namely a TypeError complaining about the arguments to raise,
instead of the exception intended.)

Such cases can be made rarer by prolonging the phase-in period; they
cannot be made impossible short of issuing at compile-time a warning for
every raise statement.

Rejection

If this PEP were accepted, nearly all existing Python code would need to
be reviewed and probably revised; even if all the above arguments in
favour of explicit instantiation are accepted, the improvement in
clarity is too minor to justify the cost of doing the revision and the
risk of new bugs introduced thereby.

This proposal has therefore been rejected[5].

Note that string exceptions are slated for removal independently of this
proposal; what is rejected is the removal of implicit exception
instantiation.

Summary of Discussion

A small minority of respondents were in favour of the proposal, but the
dominant response was that any such migration would be costly out of
proportion to the putative benefit. As noted above, this point is
sufficient in itself to reject the PEP.

New-Style Exceptions

Implicit instantiation might conflict with future plans to allow
instances of new-style classes to be used as exceptions. In order to
decide whether to instantiate implicitly, the raise machinery must
determine whether the first argument is a class or an instance -- but
with new-style classes there is no clear and strong distinction.

Under this proposal, the problem would be avoided because the exception
would already have been instantiated. However, there are two plausible
alternative solutions:

1.  Require exception types to be subclasses of Exception, and
    instantiate implicitly if and only if :

        issubclass(firstarg, Exception)

2.  Instantiate implicitly if and only if :

        isinstance(firstarg, type)

Thus eliminating implicit instantiation entirely is not necessary to
solve this problem.

Ugliness of Explicit Instantiation

Some respondents felt that the explicitly instantiating syntax is
uglier, especially in cases when no arguments are supplied to the
exception constructor:

    raise TypeError()

The problem is particularly acute when the exception instance itself is
not of interest, that is, when the only relevant point is the exception
type:

    try:
        # ... deeply nested search loop ...
            raise Found
    except Found:
        # ...

In such cases the symmetry between raise and except can be more
expressive of the intent of the code.

Guido opined that the implicitly instantiating syntax is "a tad
prettier" even for cases with a single argument, since it has less
punctuation.

Performance Penalty of Warnings

Experience with deprecating apply() shows that use of the warning
framework can incur a significant performance penalty.

Code which instantiates explicitly would not be affected, since the
run-time checks necessary to determine whether to issue a warning are
exactly those which are needed to determine whether to instantiate
implicitly in the first place. That is, such statements are already
incurring the cost of these checks.

Code which instantiates implicitly would incur a large cost: timing
trials indicate that issuing a warning (whether it is suppressed or not)
takes about five times more time than simply instantiating, raising, and
catching an exception.

This penalty is mitigated by the fact that raise statements are rarely
on performance-critical execution paths.

Traceback Argument

As the proposal stands, it would be impossible to use the traceback
argument to raise conveniently with all 2.x versions of Python.

For compatibility with versions < 2.4, the three-argument form must be
used; but this form would produce warnings with versions >= 2.4. Those
warnings could be suppressed, but doing so is awkward because the
relevant type of warning is issued at compile-time.

If this PEP were still under consideration, this objection would be met
by extending the phase-in period. For example, warnings could first be
issued in 3.0, and become errors in some later release.

References

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 End:

[1] "Standard Exception Classes in Python 1.5", Guido van Rossum.
http://www.python.org/doc/essays/stdexceptions.html

[2] "Standard Exception Classes in Python 1.5", Guido van Rossum.
http://www.python.org/doc/essays/stdexceptions.html

[3] "Standard Exception Classes in Python 1.5", Guido van Rossum.
http://www.python.org/doc/essays/stdexceptions.html

[4] "Python Language Reference", Guido van Rossum.
http://docs.python.org/reference/simple_stmts.html#raise

[5] Guido van Rossum, 11 June 2003 post to python-dev.
https://mail.python.org/pipermail/python-dev/2003-June/036176.html