Search code examples
pythonpython-3.xexceptionnameerror

Why does Python 3 raise NameError if name is bound by except...as?


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.


Solution

  • 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