Python's documentation says:
If no expressions are present,
raise
re-raises the last exception that was active in the current scope.
(Python 3: https://docs.python.org/3/reference/simple_stmts.html#raise; Python 2.7: https://docs.python.org/2.7/reference/simple_stmts.html#raise.)
However, the notion of "last active" seems to have changed. Witness the following code sample:
#
from __future__ import print_function
import sys
print('Python version =', sys.version)
try:
raise Exception('EXPECTED')
except:
try:
raise Exception('UNEXPECTED')
except:
pass
raise # re-raises UNEXPECTED for Python 2, and re-raises EXPECTED for Python 3
which results in something I didn't expect with Python 2:
Python version = 2.7.15 (v2.7.15:ca079a3ea3, Apr 30 2018, 16:30:26) [MSC v.1500 64 bit (AMD64)]
Traceback (most recent call last):
File "./x", line 10, in <module>
raise Exception('UNEXPECTED')
Exception: UNEXPECTED
but has the expected (by me) result with Python 3:
Python version = 3.6.8 (default, Feb 14 2019, 22:09:48)
[GCC 7.4.0]
Traceback (most recent call last):
File "./x", line 7, in <module>
raise Exception('EXPECTED')
Exception: EXPECTED
and
Python version = 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)]
Traceback (most recent call last):
File "./x", line 7, in <module>
raise Exception('EXPECTED')
Exception: EXPECTED
So what does "the last ... active" mean? Is there some documentation on this breaking change? Or is this a Python 2 bug?
And more importantly: What is the best way to make this work in Python 2? (Preferably such that the code will keep working in Python 3.)
Note that if one changes the code to
#
from __future__ import print_function
import sys
print('Python version =', sys.version)
def f():
try:
raise Exception('UNEXPECTED')
except:
pass
try:
raise Exception('EXPECTED')
except:
f()
raise # always raises EXPECTED
then things start to work for Python 2 as well:
Python version = 2.7.15 (v2.7.15:ca079a3ea3, Apr 30 2018, 16:30:26) [MSC v.1500 64 bit (AMD64)]
Traceback (most recent call last):
File "./x", line 13, in <module>
raise Exception('EXPECTED')
Exception: EXPECTED
I'm considering to switch to that...
Only an exception handler (or a function that a handler calls, directly or indirectly) can use raise without any expressions.
You can use the raise statement anywhere, although if not in an except block you must specify the error (ex: raise or raise TypeError('wrong type')).
The Python 2 behavior is not so much a bug as a design flaw. It was addressed in Python 3.0 by adding the exception chaining features. The closest thing to documentation of this change can be found in PEP 3134 -- Exception Chaining and Embedded Tracebacks motivation:
During the handling of one exception (exception A), it is possible that another exception (exception B) may occur. In today's Python (version 2.4), if this happens, exception B is propagated outward and exception A is lost.
This is exactly what you're seeing in 2.7: EXPECTED (A) was lost because UNEXPECTED (B) appeared and overwrote it. With the newer exception chaining features in Python 3, the full context of both errors can be preserved via __cause__
and __context__
attributes on exception instances.
For a more direct cross-compatible workaround, I would encourage you to keep the references manually, explicitly show which error is being re-raised, and as usual avoid bare except
statements (which are always too broad):
try:
raise Exception('EXPECTED')
except Exception as err_expected:
try:
raise Exception('UNEXPECTED')
except Exception as err_unexpected:
pass
raise err_expected
Should you wish to suppress the exception-chaining feature in a cross-compatible way, you can do that by setting err_expected.__cause__ = None
before re-raising.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With