You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
locals() in a module or class scope comprehension does not include the iteration var(s) since 3.12b1 #105340
In Python 3.11, inside a comprehension, locals() was bound to the scope of the comprehension:
$ py -3.11 -c "print([locals() for name in 'abc'])"
[{'.0': <str_ascii_iterator object at 0x10472dc30>, 'name': 'c'}, {'.0': <str_ascii_iterator object at 0x10472dc30>, 'name': 'c'}, {'.0': <str_ascii_iterator object at 0x10472dc30>, 'name': 'c'}]
Thanks for the reference. I do think the current behavior violates the PEP, which indicates that "locals() includes outer variables", suggesting that locals() would continue to include inner variables, and confirmed by the example where "under this PEP", x is found in locals().
In my example above, name does not appear in locals(), which is a departure from the prior behavior and of the PEP.
This seems to me to be more correct than the alternative (replacing the existing name for the duration of the list comprehension) in particular because globals are visible to outside code. Is there a practical problem with not including the loop variable in the locals() at the global level?
Is there a practical problem with not including the loop variable in the locals() at the global level?
I'm not sure I understand the question or if it was even directed at me.
The problem I see is that it's no longer possible to reference locals()['name'] inside the loop.
>>> [locals()['name'] for name in 'x']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'name'
Any code that depends on locals actually representing the local variables will fail. I linked to the code that started failing as a result of this change... but as I mentioned, that code used a legacy construct that's superseded by f-strings. I'm not yet aware of any other practical uses. In general, this change feels like a breaking change deserving of a deprecation period.
I do agree that this is a behavior change that wasn't called out in PEP 709 and that we didn't realize would be an issue before. PEP 709 says that locals() now includes the variables from the outer frame. That's true in functions, where locals() now includes both outer and comprehension-local variables, but in module and class scopes, it now includes only the outer variables.
>>> class F:
... print([locals() for i in range(1)])
...
[{'__module__': '__main__', '__qualname__': 'F'}]
>>> [locals() for i in range(1)]
[{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'F': <class '__main__.F'>}]
>>> def f(arg):
... return [locals() for i in range(1)]
...
>>> f(1)
[{'arg': 1, 'i': 0}]
And that can indeed break code like the code @jaraco linked above.
To fix it, I wonder if inside the implementation of locals() we can detect that we're within a class- or module-scoped comprehension, and if so, add the comprehension locals to the returned dictionary.
Yes, the lack of name key in locals() in the given example is a problem. Thanks @jaraco for the report.
I'm traveling this week but will be able to look into possible fixes here more closely next week. It's tricky because we can't actually add the key to the module globals (or class "locals") dictionary without potentially stomping on an existing outer-scoped variable, which would be much worse. There may be potential band-aids where locals() returns a different dictionary within a comprehension, with the extra key added. The long term solution to this is something like PEP 667, which would allow fixing this much more cleanly.
Reacted by Jelle Zijlstra, sunmy2019 and Jason R. Coombs
changed the title [-]locals() bound to globals() inside comprehension since 3.12b1[/-][+]locals() in a module or class scope comprehension does not include the iteration var(s) since 3.12b1[/+]on Jun 6, 2023
Activity
JelleZijlstra commentedon Jun 6, 2023
Yes, this is intentional and called out in PEP-709. cc @carljm
sunmy2019 commentedon Jun 6, 2023
With PEP-709, list comprehensions do not have their own stack. This affects top-level sentences.
jaraco commentedon Jun 6, 2023
Thanks for the reference. I do think the current behavior violates the PEP, which indicates that "locals() includes outer variables", suggesting that locals() would continue to include inner variables, and confirmed by the example where "under this PEP",
xis found in locals().In my example above,
namedoes not appear inlocals(), which is a departure from the prior behavior and of the PEP.sunmy2019 commentedon Jun 6, 2023
The correct thing seems to be something like
Yhg1s commentedon Jun 6, 2023
namedoesn't appear in locals() at the global level because the generated bytecode uses fast locals for the name even at the global scope:This seems to me to be more correct than the alternative (replacing the existing name for the duration of the list comprehension) in particular because globals are visible to outside code. Is there a practical problem with not including the loop variable in the locals() at the global level?
jaraco commentedon Jun 6, 2023
I'm not sure I understand the question or if it was even directed at me.
The problem I see is that it's no longer possible to reference
locals()['name']inside the loop.Any code that depends on locals actually representing the local variables will fail. I linked to the code that started failing as a result of this change... but as I mentioned, that code used a legacy construct that's superseded by f-strings. I'm not yet aware of any other practical uses. In general, this change feels like a breaking change deserving of a deprecation period.
JelleZijlstra commentedon Jun 6, 2023
I do agree that this is a behavior change that wasn't called out in PEP 709 and that we didn't realize would be an issue before. PEP 709 says that locals() now includes the variables from the outer frame. That's true in functions, where locals() now includes both outer and comprehension-local variables, but in module and class scopes, it now includes only the outer variables.
And that can indeed break code like the code @jaraco linked above.
To fix it, I wonder if inside the implementation of
locals()we can detect that we're within a class- or module-scoped comprehension, and if so, add the comprehension locals to the returned dictionary.carljm commentedon Jun 6, 2023
Yes, the lack of
namekey inlocals()in the given example is a problem. Thanks @jaraco for the report.I'm traveling this week but will be able to look into possible fixes here more closely next week. It's tricky because we can't actually add the key to the module globals (or class "locals") dictionary without potentially stomping on an existing outer-scoped variable, which would be much worse. There may be potential band-aids where
locals()returns a different dictionary within a comprehension, with the extra key added. The long term solution to this is something like PEP 667, which would allow fixing this much more cleanly.[-]locals() bound to globals() inside comprehension since 3.12b1[/-][+]locals() in a module or class scope comprehension does not include the iteration var(s) since 3.12b1[/+]pythongh-105340: include hidden fast-locals in locals()
pythongh-105340: include hidden fast-locals in locals()
gh-105340: include hidden fast-locals in locals() (#105715)
pythongh-105340: include hidden fast-locals in locals() (pythonGH-105715
[3.12] gh-105340: include hidden fast-locals in locals() (GH-105715) (#…