try/finally - what is Python doing to the local namespace on an exception?

Viewed 121

This is not a duplicate of the various why does a finally suppress my exception questions.

Instead, I find that finally's local variables are unexpected, but only in the case of an exception. In that case, the exception disappears.

(This is on Python 3.8)

def test(divisor):
    print(f"\n\ntest({divisor=})")
    exc = None  #  always assigned!
    foo = 1

    print(f"  ante.{exc=}")

    try:
        _ = 1 / divisor
        print(f"  post.{exc=}")


    except (Exception,) as exc: 
        print(f"  except.{exc=}")
    else:
        print(f"  else.{exc=}")
    finally:
        print(f"  finally:{locals()=}")

        #at this point, it should be either None 
        #whatever was caught in the except clause
        print(f"  finally.{exc=}")

test(1)

test(0)

Output in the case of success - as expected:

test(divisor=1)
  ante.exc=None
  post.exc=None
  else.exc=None
  finally:locals()={'divisor': 1, 'exc': None, 'foo': 1, '_': 1.0}
  finally.exc=None

On an exception - UnboundLocalError

It looks like the local namespace had exc deleted and this causes an UnboundLocalError error.

I'd expect it to have the ZeroDivisionError. At most, if except was defining a local scope for some reason, it could be still be None. But it's just gone.

as if del locals()["exc"] had taken place.

test(divisor=0)
  ante.exc=None
  except.exc=ZeroDivisionError('division by zero')
  finally:locals()={'divisor': 0, 'foo': 1}
Traceback (most recent call last):
  File "test_195_finally.py:27", in <module>
    test(0)
  File "test_195_finally.py:23", in test
    print(f"  finally.{exc=}")
UnboundLocalError: local variable 'exc' referenced before assignment

Binding exc to another variable shows that other variable is alive and well.

    except (Exception,) as exc: 
        exc2=exc
  finally:locals()={'divisor': 0, 'foo': 1, 'exc2': ZeroDivisionError('division by zero')}
1 Answers

Citing this thread about the same issue in the Python bug tracker, this is expected behavior

[This] happens because we need to clean the exception variable outside the except block to avoid reference cycles. If you need the variable later, you need to do an assignment [..]

... which is also documented:

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

So to keep the object reference, you need to store it in a differently named variable inside the except block.

Related