0

Why does Python 3 raise a NameError here? The name error is defined in the first line, and assigned to in the try...except block. Is this a bug in the interpreter, or am I missing a subtle change in the language definition from Python 2 to 3?

error = None

try:
    raise Exception('Boom!')
except Exception as error:
    pass

if error is not None:
    raise error

This is the traceback when executed with Python 3.6.7:

$ python3 nameerror.py
Traceback (most recent call last):
  File "nameerror.py", line 8, in <module>
    if error is not None:
NameError: name 'error' is not defined

With Python 2.7.15, we get the expected Boom!:

$ python2 nameerror.py
Traceback (most recent call last):
  File "nameerror.py", line 9, in <module>
    raise error
Exception: Boom!

If the code is wrapped in a function, Python 3.6.7 raises UnboundLocalError instead, while Python 2.7.15 still works as expected.

$ python3 unbound.py
Traceback (most recent call last):
  File "unbound.py", line 13, in <module>
    main()
  File "unbound.py", line 9, in main
    if error is not None:
UnboundLocalError: local variable 'error' referenced before assignment

Curiously, removing the as error from the exception handler fixes the NameError resp. UnboundLocalError.

Claudio
  • 3,089
  • 2
  • 18
  • 22
  • 1
    Because of scoping. The `except` block has its own scope. `error` is not defined outside of the block. – Håken Lid Nov 06 '18 at 12:03
  • 3
    @HåkenLid: Nope, it's not its own scope. Any other variable defined in the `except` block survives (because Python is function scoped, not block scoped). It's a specific feature of the capture variable itself, not the block. – ShadowRanger Nov 06 '18 at 12:06
  • You're right. It's only the specific error name that is cleared from the scope when the exception block exits. – Håken Lid Nov 06 '18 at 12:16

1 Answers1

4

This was an intentional change in the except semantics to resolve an issue wherein reference cycles were formed between frames in a traceback and the exception in the frame:

In order to resolve the garbage collection issue related to PEP 344, except statements in Python 3 will generate additional bytecode to delete the target, thus eliminating the reference cycle. The source-to-source translation, as suggested by Phillip J. Eby [9], is

try:
    try_body
except E as N:
    except_body
...

gets translated to (in Python 2.5 terms)

try:
    try_body
except E, N:
    try:
        except_body
    finally:
        N = None
        del N
...

You can preserve the original exception simply by assigning it to some other name, e.g.:

try:
    raise Exception('Boom!')
except Exception as error:
    saved_error = error  # Use saved_error outside the block
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271