Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Name binding in `except` clause deleted after the clause [duplicate]

How can I stop Python from deleting a name binding, when that name is used for binding the exception that is caught? When did this change in behaviour come into Python?

I am writing code to run on both Python 2 and Python 3:

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

print(text_template.format(exc=exc))

Notice that exc is explicitly bound before the exception handling, so Python knows it is a name in the outer scope.

On Python 2.7, this runs fine and the exc name survives to be used in the format call::

Got exception: ZeroDivisionError

Great, this is exactly what I want: The except clause binds the name and I can use that name in the rest of the function to refer to the exception object.

On Python 3.5, the format call fails because apparently the exc binding is deleted::

Traceback (most recent call last):
  File "<stdin>", line 8, in <module>
NameError: name 'exc' is not defined

Why is the exc binding deleted from the outer scope? How are we meant to reliably preserve the name binding to use it after the except clause?

When did this change come into Python, where is it documented?

Would I be right to report this as a bug in Python 3?

like image 385
bignose Avatar asked Jul 25 '17 01:07

bignose


1 Answers

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
like image 64
Christian Dean Avatar answered Sep 19 '22 03:09

Christian Dean