Python 'raise' without arguments: what is "the last exception that was active in the current scope"?
Asked Answered
E

2

11

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...

Emmalynn answered 3/6, 2019 at 14:45 Comment(1)
Related: cosmicpercolator.com/2016/01/13/…Halloo
H
5

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.

Halloo answered 4/6, 2019 at 4:8 Comment(0)
S
0

raise uses the same information as sys.exc_info, which documents both behaviors. Since the per-frame behavior that your workaround exploits is documented, that’s the way to go.

PEP 3110 made several changes to the except statement. I believe it included this one, but the only thing explicitly mentioned is that an exception stored by as is discarded when leaving the except.

Snailpaced answered 4/6, 2019 at 3:41 Comment(2)
This PEP 3110 is mostly unrelated to OP question. The discarding of exception when leaving the except block is about working around an exception -> traceback -> stack frame -> exception reference cycle that the new __traceback__ attribute caused.Halloo
@wim: Well, the behavior seems to have been changed by r62847, but I haven’t found what (other?) PEP that might be.Snailpaced

© 2022 - 2024 — McMap. All rights reserved.