Description
It is already the case in all recent Python versions that the lazy-update behavior of the dictionary returned from locals()
causes weird frame-introspection-sensitive semantics around lazy iteration of locals()
. E.g. this code will work fine:
def f(a, b):
ret = {}
for k, v in locals().items():
ret[k] = v
return ret
f(1, 2)
But run this under a tracing function that simply accesses frame.f_locals
:
import sys
def tracefunc(frame, *a, **kw):
frame.f_locals
return tracefunc
sys.settrace(tracefunc)
f(1, 2)
And suddenly you instead get RuntimeError: dictionary changed size during iteration
.
The reason is because accessing frame.f_locals
triggers a lazy update of the cached locals dictionary on the frame from the fast locals array. And this is the same dictionary returned by locals()
(it doesn't create a copy), and so the tracing function has the side effect of adding the keys k
and v
to the locals dictionary, while it is still being lazily iterated over. Without the access of frame.f_locals
, nothing triggers this locals-dict lazy update after the initial call to locals()
, so the iteration is fine.
This is a pre-existing problem with the semantics of locals()
generally, and there are proposals to fix it, e.g. PEP 667.
What is new in Python 3.12 is that PEP 709 means comprehensions can newly be exposed to this same issue. Consider this version of the above function:
def f(a, b):
return {k: v for k, v in locals.items()}
Under PEP 709, k
and v
are now part of the locals of f
(albeit isolated and only existing during the execution of the comprehension), which means this version of f
is now subject to the same issue as the previous for-loop version, if run under a tracing func that accesses f_locals
.