Tell compiler about a local it doesn't know about?
Question: I have a function called `let`, which modifies the calling namespace to insert
a new variable.

    
    
    def let(**nameValuePair):
        from inspect import stack
    
        name, value = nameValuePair.items()[0]
        stack()[1][0].f_locals[name] = value
        return value
    

The idea is it allows you insert assignment statements anywhere you want, even
where assignment statements aren't typically allowed in Python (albeit, it
requires 5 extra characters for `let()`.

This works perfectly when called from the global namespace.

    
    
    >>> let(outside = 'World')
    >>> print(outside)
    World
    

This fails with the error `NameError: global name 'hello' is not defined`:

    
    
    >>> def breaker():
    ...     let(hello = 'World')
    ...     print(hello)
    ...
    >>> breaker()
    

You might jump to the conclusion that the issue is with my `let` function. It
is not - my function performs perfectly. To show you that:

    
    
    >>> def fine():
    ...     let(another = 'World')
    ...     print(locals()['another'])
    ...
    >>> fine()
    World
    

The issue actually comes from when the Python interpreter compiles `breaker`.
To demonstrate, I'll create another function which does the same thing as
`breaker`, but using an ordinary assignment instead of `let`, and then I'll
decompile both with `dis`.

    
    
    >>> def normal():
    ...     hello = 'World'
    ...     print(hello)
    ...
    >>> normal()
    World
    
    >>> from dis import dis
    >>> print(dis(normal))
     0 LOAD_CONST               1 ('World')
     3 STORE_FAST               0 (hello)
     6 LOAD_FAST                0 (hello)
     9 PRINT_ITEM
    10 PRINT_NEWLINE
    11 LOAD_CONST               0 (None)
    14 RETURN_VALUE
    
    >>> print(dis(breaker))
     0 LOAD_GLOBAL              0 (let)
     3 LOAD_CONST               1 ('hello')
     6 LOAD_CONST               2 ('World')
     9 CALL_FUNCTION          256
    12 POP_TOP
    13 LOAD_GLOBAL              2 (hello)  # <= This is the real problem.
    16 PRINT_ITEM
    17 PRINT_NEWLINE
    18 LOAD_CONST               0 (None)
    19 RETURN_VALUE
    

The problem is, the compiler is looking over `breaker`, doesn't see that
`hello` gets assigned anywhere in the local scope, and so assumes that it must
be a global variable.

What can I do about this? When my function gets called, could I, for example,
swap out the problematic line that I indicated and replace it with:

    
    
    LOAD_GLOBAL 1 (locals)
    CALL_FUNCTION 0
    LOAD_CONST 1 ('hello')  # Or whatever the name of the variable is
    BINARY_SUBSCR
    

I know that I could quickly fix the immediate problem by simply storing the
variable in the global scope, but that could lead to lots of subtle bugs. To
name a few:

  1. Global variables get written over.
  2. Objects might persist in memory for far longer than intended.
  3. Something that would be a typo otherwise, is instead valid. (admittedly this one seems quite unlikely).

... Actually, if I can change the compiled code, I could even add a
`STORE_GLOBAL` or `DELETE_GLOBAL` after `RETURN_VALUE`, perhaps? Cache the
former global variable elsewhere or something?


Answer: The documentation for `locals()` notes the following:

> **Note** The contents of this dictionary should not be modified; changes may
> not affect the values of local and free variables used by the interpreter.

I checked and don't see an explicit note in the `inspect` docs about
`f_locals` but I don't see why it would be any different.

