Except-Clause Deletes Local Variable

Name binding in `except` clause deleted after the clause

No this is not a bug. The behavior you are experiencing is clearly and explicitly defined in the Python 3 documentation for the try/except statement. The reason for this behavior is also given:

When an exception has been assigned using as target, it is cleared at the end of the except clause. This is as if

except E as N:
foo

was translated to

except E as N:
try:
foo
finally:
del N

This means the exception must be assigned to a different name to be able to refer to it after the except clause. Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.

The reason declaring the name outside of the scope of the try/except block didn't work is because you used exc in the as clause. So that was the name Python deleted.

The fix is to use a different name in the as clause to bind the exception to, and then assign the global variable to the different exception name:

>>> exc_global = None
>>> try:
1 / 0
text_template = "All fine!"
except ZeroDivisionError as exc:
exc_global = exc
text_template = "Got exception: {exc.__class__.__name__}"

>>> print(text_template.format(exc=exc_global))
Got exception: ZeroDivisionError

As Anthony Sottile noted in the comments, the disassembly for the try/except code also clearly supports the above statements made by the documentation:

>>> code = """
try:
1/0
text_template = "All fine!"
except ZeroDivisionError as exc:
text_template = "Got exception: {exc.__class__.__name__}"
"""
>>> from dis import dis
>>> dis(code)
2 0 SETUP_EXCEPT 16 (to 18)

3 2 LOAD_CONST 0 (1)
4 LOAD_CONST 1 (0)
6 BINARY_TRUE_DIVIDE
8 POP_TOP

4 10 LOAD_CONST 2 ('All fine!')
12 STORE_NAME 0 (text_template)
14 POP_BLOCK
16 JUMP_FORWARD 38 (to 56)

5 >> 18 DUP_TOP
20 LOAD_NAME 1 (ZeroDivisionError)
22 COMPARE_OP 10 (exception match)
24 POP_JUMP_IF_FALSE 54
26 POP_TOP
28 STORE_NAME 2 (exc)
30 POP_TOP
32 SETUP_FINALLY 10 (to 44)

6 34 LOAD_CONST 3 ('Got exception: {exc.__class__.__name__}')
36 STORE_NAME 0 (text_template)
38 POP_BLOCK
40 POP_EXCEPT
42 LOAD_CONST 4 (None)
>> 44 LOAD_CONST 4 (None)
46 STORE_NAME 2 (exc)
48 DELETE_NAME 2 (exc)
50 END_FINALLY
52 JUMP_FORWARD 2 (to 56)
>> 54 END_FINALLY
>> 56 LOAD_CONST 4 (None)
58 RETURN_VALUE

Python 3 exception deletes variable in enclosing scope for unknown reason

Quoting the documentation of try,

When an exception has been assigned using as target, it is cleared at the end of the except clause. This is as if

except E as N:
foo

was translated to

except E as N:
try:
foo
finally:
del N

This means the exception must be assigned to a different name to be able to refer to it after the except clause. Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.

This is covered in these two PEPs.

  1. PEP 3110 - Catching Exceptions in Python 3000

  2. PEP 344 - Exception Chaining and Embedded Tracebacks

why variable used in except is removed

This is explained in the documentation (parts in [ ] braces added by me):

When an exception has been assigned using as target [i.e. as x in your case], it is cleared at the end of the except clause. This is as if

except E as N:
foo

was translated to:

except E as N:
try:
foo
finally:
del N

This means the exception must be assigned to a different name to be able to refer to it [i.e. this name, x in your case] after the except clause.

The reason for this is also stated:

Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.

why variable used in except is removed

This is explained in the documentation (parts in [ ] braces added by me):

When an exception has been assigned using as target [i.e. as x in your case], it is cleared at the end of the except clause. This is as if

except E as N:
foo

was translated to:

except E as N:
try:
foo
finally:
del N

This means the exception must be assigned to a different name to be able to refer to it [i.e. this name, x in your case] after the except clause.

The reason for this is also stated:

Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.

except Foo as bar causes bar to be removed from scope

msg in the except clause is in the same scope as msg on the first line.

But in Python 3 we have this new behavior too:

When an exception has been assigned using as target, it is cleared at
the end of the except clause. This is as if

except E as N:
foo

was translated to

except E as N:
try:
foo
finally:
del N

This means the exception must be assigned to a different name to be
able to refer to it after the except clause. Exceptions are cleared
because with the traceback attached to them, they form a reference
cycle with the stack frame, keeping all locals in that frame alive
until the next garbage collection occurs.

so, you "overwrite msg" in the exception handler, and exiting the handler will delete the variable to clear the traceback reference cycle.

Variable scoping with an except block: Difference between Python 2 and 3

This behaviour was introduced in Python 3, to prevent reference cycles, because the exception target - arithmetic_error in the question - keeps a reference to the traceback.

From the Language Reference

When an exception has been assigned using as target, it is cleared at
the end of the except clause. This is as if

except E as N:
foo

was translated to

except E as N:
try:
foo
finally:
del N

This means the exception must be assigned to a different name to be
able to refer to it after the except clause. Exceptions are cleared
because with the traceback attached to them, they form a reference
cycle with the stack frame, keeping all locals in that frame alive
until the next garbage collection occurs.

This was originally documented in PEP3110.

What is the scope of the `as` binding in an `except` statement or context manager?

As explained in PEP 3110, as well as current documentation, variables bound with as in an except block are explicitly and specially cleared at the end of the block, even though they share the same local scope. This improves the immediacy of garbage collection. The as syntax was originally not available for exceptions in 2.x; it was backported for 2.6, but the old semantics were preserved.

The same does not apply to with blocks:

>>> from contextlib import contextmanager
>>> @contextmanager
... def test():
... yield
...
>>> with test() as a:
... pass
...
>>> a # contains None; does not raise NameError
>>>
>>> def func(): # similarly within a function
... with test() as a:
... pass
... return a
...
>>> func()
>>>

The behaviour is specific to the except block, not to the as keyword.

Variable scope in case of an exception in python

Simple: while does not create a scope in Python. Python has only the following scopes:

  • function scope (may include closure variables)
  • class scope (only while the class is being defined)
  • global (module) scope
  • comprehension/generator expression scope

So when you leave the while loop, e, being a local variable (if the loop is in a function) or a global variable (if not), is still available.

tl;dr: Python is not C.



Related Topics



Leave a reply



Submit