PEP 667 question about impact on function local variable lookup

PEP 667 has this example (slightly modified):

def l():
    "Get the locals of caller"
    return sys._getframe(1).f_locals
def test():
    if 0: y = 1 # Make 'y' a local variable
    x = 1
    l()['x'] = 2
    l()['y'] = 4
    l()['z'] = 5
    print(locals(), x)

test() will print

{'x': 2, 'y': 4, 'z': 5} 2

I see that x and y are local variables, but z is only there becuse it was inserted in l().

Under Python 3.9, the three print()s compile as:

LOAD_GLOBAL              1 (print)
LOAD_GLOBAL              2 (locals)
CALL_FUNCTION            0
LOAD_FAST                0 (x)
CALL_FUNCTION            2

LOAD_GLOBAL              1 (print)
LOAD_FAST                1 (y)
CALL_FUNCTION            2

LOAD_GLOBAL              1 (print)
LOAD_GLOBAL              3 (z)
CALL_FUNCTION            2

And under Python 3.9, calling test() prints {'x': 1, 'z': 5} 1. Either of the other two print()s raise an error, because y is unbound and z is undefined.

If z is a free variable captured from an enclosing function, then the LOAD_GLOBAL would be LOAD_DEREF instead.

My questions are:

  • What gets compiled for accessing these three variables? I’m hoping that x and y use LOAD_FAST and z uses LOAD_NAME
  • Will the accesses to x and y still be fast? If not, then this is a big performance hit, defeating the whole LOAD_NAME optimization.
    = What happens with accessing z? It’s clear to me that locals() will be in sync with f_locals, and both will contain z : 5. But does this also carry over to print(z)? Or will z be a global variable?
  • Make sure that LOAD_FAST is still compiled for x and y.
  • I suggest that the PEP make the answer clear, regarding z.
  • And perhaps you might consider compiling with LOAD_NAME for z instead of LOAD_GLOBAL or LOAD_DEREF. This would be slightly slower by checking f_locals first, but it would mean that updates to locals() would show up as local variables.
    You could optimize this by making a note of whether f_locals has ever been updated for a name not in fastlocals, and if not, it would not check f_locals (knowing that LOAD_NAME is never called when LOAD_FAST would do).

The bytecode is an implementation detail, so is out of scope for the PEP. But don’t worry, nothing will go slower.

Python is lexically scoped, not dynamically scoped, so z remains a global variable regardless of l()['z'] = 5.
This is why I included the code if 0: y = 1 so that y would be a local variable, even though it is undefined.