4

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')}
JL Peyret
  • 10,917
  • 2
  • 54
  • 73

1 Answers1

4

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.

adjan
  • 13,371
  • 2
  • 31
  • 48
  • Txs for finding this so quickly. Doing just that, but I was really surprised at this behavior. Makes sense if there's a behind the scene reason for garbage collection. Still, unusual to find a bizarre edge case in Python. – JL Peyret Nov 03 '20 at 21:34
  • Ditto. Thanks for the explanation. Very unexpected. – Frank Yellin Nov 03 '20 at 21:36
  • Unfortunately, reading the report, it sounds like they made a bug into a feature. – Frank Yellin Nov 03 '20 at 21:47
  • 1
    I know that Python isn't a block-scoped language, but it seems like making an, er, exception to that design and giving `except` clauses their own local scope would be less surprising than the _deletion of a preexisting local_. – Mark Reed Nov 03 '20 at 21:54
  • 1
    @MarkReed not really, no. a) it would be a bigger wart than "just" nuking the variable and b) once we had a local scope/namespace to deal with it would an even bigger hassle to make `exc`s value visible to the finally clause so it would, on its own, achieve nothing. I'd have qualify with `global exc` and even then I don't know if `global` would refer to the `test` function's scope (doubt it) or the script's full global scope (more likely). – JL Peyret Nov 09 '20 at 00:18