[Bug] Micropython variable scoping confusion
Describe the bug
I am trying to access and even change local variables via the locals() disctionary.
Any local variables seem to be missing from this dictionary / except for the module global scope, where globals() seems to be equal to locals().
To reproduce
def function1():
apple = 1
print("apple", apple, locals())
locals()['apple'] = 2
print("apple", apple, locals())
def function2():
peach = 2
print("peach", peach, locals())
locals()['peach'] = 3
print("peach", peach, locals())
def function3():
pear = 3
print("pear", pear, locals())
locals()['pear'] = 4
print("pear", pear, locals())
function1()
function2()
varroot1 = 42
print("varroot1", varroot1, locals())
locals()['varroot1'] = 43
print("varroot1", varroot1, locals())
function3()
varroot1 42 {'__name__': '__main__', 'function1': <function>, 'varroot1': 42, 'function3': <function>, 'function2': <function>}
varroot1 43 {'__name__': '__main__', 'function1': <function>, 'varroot1': 43, 'function3': <function>, 'function2': <function>}
pear 3 {'__name__': '__main__', 'function1': <function>, 'varroot1': 43, 'function3': <function>, 'function2': <function>}
pear 3 {'__name__': '__main__', 'function1': <function>, 'varroot1': 43, 'pear': 4, 'function3': <function>, 'function2': <function>}
apple 1 {'__name__': '__main__', 'function1': <function>, 'varroot1': 43, 'pear': 4, 'function3': <function>, 'function2': <function>}
apple 1 {'apple': 2, '__name__': '__main__', 'pear': 4, 'function3': <function>, 'function2': <function>, 'function1': <function>, 'varroot1': 43}
peach 2 {'apple': 2, '__name__': '__main__', 'pear': 4, 'function3': <function>, 'function2': <function>, 'function1': <function>, 'varroot1': 43}
peach 2 {'apple': 2, 'peach': 3, '__name__': '__main__', 'pear': 4, 'function3': <function>, 'function2': <function>, 'function1': <function>, 'varroot1': 43}
Expected behavior Data is set to the new values in the original local scoped variables.
Here only varroot1 variable is changing values.
Screenshots Code snippet is added.
Hi Attila,
In the python doc about locals
In an [optimized scope](https://docs.python.org/3/glossary.html#term-optimized-scope)
(including functions, generators, and coroutines),
each call to locals() instead returns a fresh dictionary containing the current bindings
of the function’s local variables and any nonlocal cell references.
In this case, name binding changes made via the returned dict are not written back
to the corresponding local variables or nonlocal cell references, and assigning, reassigning,
or deleting local variables and nonlocal cell references does not affect the contents
of previously returned dictionaries.
Might this be what you see happening?
Per python3.13 there is a change in this, but pybricks micropython is? not at 3.13, but I can not find what version it really is.
MicroPython is not Python. There is no locals() dict in MicroPython and the local values just aren't accessible.
See https://docs.micropython.org/en/latest/genrst/core_language.html#local-variables-aren-t-included-in-locals-result
Thanks David.
Thank you Bert, thank you David.
One night sleep, your in-depth comments and an awesome days with FLL kids helped!
I think the below concept will work
x = 10
def externally_set_variable(**kwargs):
print(kwargs)
kwargs['a'] = 43
kwargs['x'] = 45
return kwargs
def testfn1():
global x
a = 12
print(">>externally_set_variable before", a, x)
a,x = externally_set_variable(a=a,x=x).values()
print(">>externally_set_variable after", a, x)
testfn1()
>>externally_set_variable before 12 10
{'a': 12, 'x': 10}
>>externally_set_variable after 43 45
Background: It would be awesome to get/set local variables via the vscode debugger. This approach still limits me to capture compile-time parsed and injected variables, but that is still better than nothing! With globals()['x'] I would be able to read/write ad-hoc, but as micropython is optimized, I understand this is not really needed and not available for everyday use...
x = 10
def dap_trap(filename, lineno, **kwargs):
print('send available variable names and values as:', kwargs.keys(), kwargs.values())
print(kwargs, kwargs.values())
kwargs['a'] = 43
kwargs['i'] = 44
kwargs['x'] = 45
print(kwargs.values())
return kwargs.values()
def testfn1():
global x
a = 10
i = 20
i,a,x = dap_trap('filename1.py', 42, i=i, a=a, x=x)
testfn1()
print(x)
I would suggest updating pybricks documentation for locals()
https://github.com/pybricks/pybricks-api/blob/32f974e7e549ab4e44e2f69efb0c6c1187d47f9b/src/ubuiltins/init.py#L822
if @dlech agrees.
In the past weeks I have looked into the issue.
The optimization mentioned above by @dlech still does not suggest why locals() does not display local variables in such a case.
def test1():
lx = 1
print(locals())
This prints locals without lx
{'hub': <PrimeHub>, 'test1': <function>, 'Color': {'BROWN': Color.BROWN, 'YELLOW': Color.YELLOW, 'GREEN': Color.GREEN, 'CYAN': Color.CYAN, 'BLACK': Color.BLACK, 'NONE': Color.NONE, 'MAGENTA': Color.MAGENTA, 'RED': Color.RED, 'BLUE': Color.BLUE, 'VIOLET': Color.VIOLET, 'GRAY': Color.GRAY, 'ORANGE': Color.ORANGE, 'WHITE': Color.WHITE}, 'PrimeHub': <class 'PrimeHub'>, '__name__': '__main__', 'wait': <function>}
When looking for an answer this is what I understood:
Extreme Optimization: Slots vs. Dictionaries in MicroPython
The issues you encountered with locals() and eval() stem from MicroPython's extreme optimization to save memory and increase execution speed on resource-constrained microcontrollers. This optimization fundamentally changes how local variables are stored, relying on slots instead of the typical dictionary structure used in standard Python (CPython).
1. The Slot Mechanism (The Core Optimization)
The slot mechanism replaces the need for a dynamic dictionary to manage local variables:
-
Compilation: When MicroPython compiles your code (e.g., to an .mpy file), it analyzes each function. For local variables like lx and ly, it calculates their required storage space and assigns them a fixed memory position, or a slot, on the function's stack frame.
- For example, lx might be assigned to Slot 0 and ly to Slot 1.
- Name Discarding: Crucially, the actual string names ('lx', 'ly') are discarded from the compiled bytecode. The compiled instructions simply refer to the index (the slot number).
- Execution Speed: This direct access via index is incredibly fast—as fast as accessing an element in a C array—and far quicker than performing a dictionary lookup based on a string name.
- Memory Savings: Because the variable names are not stored for local scope, significant memory is saved, which is vital for microcontrollers.
2. Why locals() and eval() Fail
This slot-based optimization directly breaks introspection tools that rely on finding a dictionary of names:
- locals(): The function returns a dictionary that lacks the optimized local variable names (lx, ly) because those names no longer exist in a dictionary form; they only exist as slots.
- eval('lx'): The function looks for the string name 'lx' in the local scope dictionary. Since the name is stored only in a memory slot, not in a symbol table, eval() cannot find it, resulting in a NameError.
3. Summary of Trade-off
MicroPython sacrifices runtime introspection (the ability to dynamically inspect or manipulate local variables by their string name) in exchange for maximum speed and minimal memory consumption.
To use dynamic features like eval() in MicroPython, you must explicitly manage the local scope by passing a manually created dictionary containing the necessary names and values.
This prints locals without
lx
Those look like globals to me. lx is the only local in that scope.
I would suggest updating pybricks documentation for
locals()
Yes, it is clearly not correct. We can keep the explanation simple though. Something like:
MicroPython does not support ``locals()``. Do not use this function - `it will not work as expected
<https://docs.micropython.org/en/latest/genrst/core_language.html#local-variables-aren-t-included-in-locals-result>`_.
Instead, it returns the same dictionary as ``globals()``.