One interesting quirk is that prior to 3.13, your own example actually worked:
Python 3.12.11 (main, Jun 12 2025, 00:00:00) [GCC 15.1.1 20250521 (Red Hat 15.1.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def f():
... a = 1
... print(locals()["a"])
...
>>> f()
1
>>> ns = {}
>>> exec(f.__code__, globals(), ns)
1
>>> ns
{'a': 1}
It didn’t intrinsically work, it just worked for this specific example because locals() used to implicitly call PyFrame_FastToLocals(), so calling it inside the function updated the passed in namespace as a side effect.
That side effect went away in 3.13 due to the introduction of the fast locals proxy and the switch to making locals() return independent snapshots. PyFrame_FastToLocals() itself also no longer does anything: cpython/Objects/frameobject.c at 3.13 · python/cpython · GitHub
It seems like a reasonable feature request to me, and since the behaviour has been inconsistent over time, I’m not sure it would need to be an explicit flag (the builtin exec/eval could just enable it automatically when given an optimised code block).
That said, it would be a genuinely intrusive change at the implementation level (potentially affecting the signature of public C APIs), so it’s entirely plausible that the outcome might just be that the limitations of passing optimised code blocks to exec/eval get explicitly documented (which would still be a useful docs improvement).
Edit: on a closer look at the code, handling this may not affect any public APIs, assuming the change itself is deemed acceptable. Specifically, regular optimised function calls run with a NULL f_locals value, allowing eval/exec invocations to be detected by virtue of their non-NULL f_locals values (module and class body executions don’t have the local variable optimisation flag set on their code objects, since they intentionally don’t use fast locals). It may therefore be at least theoretically possible to write fast locals changes back to a non-NULL locals namespace when popping an internal interpreter frame from the stack without slowing down regular function call evaluation (beyond the single additional NULL pointer check). There’s a non-trivial risk of unintended side effects, though, so “not supported” would still be the safer conclusion.