I would like to propose a change to exception handlers to make it harder to accidently leak names defined only in the exception handler blocks. This change follows from the decision to delete the name of an exception at the end of a handler. The goal of this change is to prevent people from relying on names that are defined only in a handler.

As an example, let's looks at a function with a try except:


def f():
    try:
        ...
    except:
        a = 1
    return a


This function will only work if the body raises some exception, otherwise we will get an UnBoundLocalError. I propose that we should make `a` fall out of scope when we leave the except handler regardless to prevent people from depending on this behavior. This will make it easier to catch bugs like this in testing. There is one case where I think the name should not fall out of scope, and that is when the name is already defined outside of the handler. For example:


def g():
    a = 1
    try:
        ...
    except:
        a = 2
    return a


I think this code is well behaved and should continue to work as it already does. There are a couple of ways to implment this new behavior but I think the simplest way to do this would be to treat the handler as a closure where all the free variables defined as nonlocal.

This would need to be a small change to the compiler but may have some performance implications for code that does not hit the except handler. If the handler is longer than the bytecode needed to create the inner closure then it may be faster to run the function when the except handler is not hit.

This changes our definition of f from:

  2           0 SETUP_EXCEPT             8 (to 11)

  3           3 LOAD_CONST               1 (Ellipsis)
              6 POP_TOP
              7 POP_BLOCK
              8 JUMP_FORWARD            14 (to 25)

  4     >>   11 POP_TOP
             12 POP_TOP
             13 POP_TOP

  5          14 LOAD_CONST               2 (1)
             17 STORE_FAST               0 (a)
             20 POP_EXCEPT
             21 JUMP_FORWARD             1 (to 25)
             24 END_FINALLY

  6     >>   25 LOAD_FAST                0 (a)
             28 RETURN_VALUE

to something more like:

f
-
  3           0 SETUP_EXCEPT             8 (to 11)

  4           3 LOAD_CONST               0 (Ellipsis)
              6 POP_TOP
              7 POP_BLOCK
              8 JUMP_FORWARD            20 (to 31)

  5     >>   11 POP_TOP
             12 POP_TOP
             13 POP_TOP
             14 LOAD_CONST               1 (<code object <excepthandler> at 0x7febcd6e2300, file "<code>", line 1>)
             17 LOAD_CONST               2 ('<excepthandler>')
             20 MAKE_FUNCTION            0
             23 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             26 POP_EXCEPT
             27 JUMP_FORWARD             1 (to 31)
             30 END_FINALLY

  7     >>   31 LOAD_FAST                0 (a)
             34 RETURN_VALUE

f.<excepthandler>
-----------------
  1           0 LOAD_CONST               0 (1)
              3 STORE_FAST               0 (a)
              6 LOAD_CONST               1 (None)
              9 RETURN_VALUE


This new code properly raises the unbound locals exception when executed. For g we could use a MAKE_CLOSURE instead of MAKE_FUNCTION.