PEP 317 – Eliminate Implicit Exception Instantiation
- Author:
- Steven Taschuk <staschuk at telusplanet.net>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 06-May-2003
- Python-Version:
- 2.4
- Post-History:
- 09-Jun-2003
Abstract
“For clarity in new code, the formraise 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 [1].
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 [1] 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:
- In the code
try: raise MyError, raised except MyError, caught: pass
the syntactic parallel between the
raise
andexcept
statements strongly suggests thatraised
andcaught
refer to the same object. For string exceptions this actually is the case, but for instance exceptions it is not. - 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.) - An implicitly instantiating
raise
statement with no arguments, such asraise MyError
simply does not do what it says: it does not raise the named object.
- 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
[3] 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, 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 [6].
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:
- Require exception types to be subclasses of
Exception
, and instantiate implicitly if and only ifissubclass(firstarg, Exception)
- 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.
Source: https://github.com/python/peps/blob/main/peps/pep-0317.rst
Last modified: 2023-09-09 17:39:29 GMT