Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 982
MarnixKlooster ReinstateMonica Avatar asked Jun 03 '19 14:06

MarnixKlooster ReinstateMonica


People also ask

Can we use raise without exception in Python?

Only an exception handler (or a function that a handler calls, directly or indirectly) can use raise without any expressions.

Can a function have a raise statement without having a try except block?

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


1 Answers

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.

like image 194
wim Avatar answered Oct 21 '22 05:10

wim