# Is there a good way to get the local namespace of a given function-derived code object from exec?

**URL:** <https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939>\
**Category:** Python Help\
**Created:** [November 19, 2025, 1:54am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939 "2025-11-19T01:54:21Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [November 19, 2025, 1:54am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/1 "2025-11-19T01:54:21Z")

</div>

I’d like to capture the local namespace at the end of the execution of a function-derived code object, but it seems that updates to fast locals can’t be propagated to the `locals` mapping supplied to `exec`:

```python
def f():
    a = 1
    print(locals()['a']) # got 1

ns = {}
exec(f. __code__ , globals(), ns)
print(ns['a']) # expected 1, got KeyError

```

Is there a good workaround?

And does it make sense that we submit a feature request for `exec` to translate fast locals back to a dict at the end of the execution so the supplied `locals` mapping can be updated?

---

<div class="post-metadata">

**Author:** ![csm10495](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/csm10495/32/9731_2.png) [@csm10495](https://discuss.python.org/u/csm10495)\
**Post date:** [November 19, 2025, 2:01am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/2 "2025-11-19T02:01:42Z")

</div>

I don’t think we can change that behavior because it’s technically incompatible with the old behavior (of not updating).

Maybe a flag could be added to allow updating though.

Can you instead return the pieces you want from `f`?

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [November 19, 2025, 2:50am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/3 "2025-11-19T02:50:43Z")

</div>

> [@csm10495](#):
>
> I don’t think we can change that behavior because it’s technically incompatible with the old behavior (of not updating).
> 
> Maybe a flag could be added to allow updating though.

Right, the said behavior needs to be optional or it would break existing code.

An optional flag such as `capture_fast_locals` to the `exec` function may indeed be the way to go.

> [@](#):
>
> Can you instead return the pieces you want from `f`?

The code object in my case is supplied by the end user, with no requirement for the function to return anything. The goal is to capture the ending state of the execution and let the user use the resulting namespace in a particular way.

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [November 19, 2025, 6:34am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/4 "2025-11-19T06:34:17Z")

</div>

One interesting quirk is that prior to 3.13, your own example actually worked:

```python
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](https://github.com/python/cpython/blob/3.13/Objects/frameobject.c#L2149)

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.

---

<div class="post-metadata">

**Author:** ![JoBe](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jobe/32/26961_2.png) [@JoBe](https://discuss.python.org/u/JoBe)\
**Post date:** [November 19, 2025, 3:57pm UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/5 "2025-11-19T15:57:19Z")

</div>

I’ve had a similar issue quite recently too, but the way I’ve gotten it solved, is by inspecting the last frames `f_locals` attribute.

Some `inspect.getcurrentframe().f_back.f_locals` would work, but some implementations do not support it / error (Attribute access on `None`).

So this should work for 90% of the cases where you use it.

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [November 20, 2025, 1:17am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/6 "2025-11-20T01:17:18Z")

</div>

> [@JoBe](#):
>
> I’ve had a similar issue quite recently too, but the way I’ve gotten it solved, is by inspecting the last frames `f_locals` attribute.
> 
> Some `inspect.getcurrentframe().f_back.f_locals` would work, but some implementations do not support it / error (Attribute access on `None`).
> 
> So this should work for 90% of the cases where you use it.

Did you do it in a profiler/tracer function? I’ve thought about this workaround too but decided that it may pose too much risk of interfering with or being interfered by the users’ profilers/debuggers to be used as a general solution.

> [@ncoghlan](#):
>
> 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](https://github.com/python/cpython/blob/3.13/Objects/frameobject.c#L2149)

Great insights. Thanks!

> [@](#):
>
> 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.

Yeah if it’s deemed OK that this is going to be the new behavior, so that there’s no need to pass down a flag from the `exec` function to the C APIs, then it does appear that this is going to be a non-intrusive change.

As I can’t imagine any meaningful real-world use for the current behavior of _no change_ to the locals dict passed in to `exec`, I’m hopeful that the said new behavior would have so little negative real-world impact that it would be deemed OK by the core devs.

---

<div class="post-metadata">

**Author:** ![JoBe](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jobe/32/26961_2.png) [@JoBe](https://discuss.python.org/u/JoBe)\
**Post date:** [November 20, 2025, 5:14am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/7 "2025-11-20T05:14:13Z")

</div>

> [@blhsing](#):
>
> Did you do it in a profiler/tracer function? I’ve thought about this workaround too but decided that it may pose too much risk of interfering with or being interfered by the users’ profilers/debuggers to be used as a general solution.

Nope.

```python
def f():
    a = 1
    print(locals()['a']) # got 1

exec(f. __code__ , globals(), {})
f = inspect.getcurrentframe()
print(f.f_back.f_back.f_locals['a']) # One f_back would be `exec` (would work too)

```

Apart from that, you could also make `f` return either `a` or even all locals. That would require you to “own” `f` though, so that you can be sure that it always returns what you need.

Iirc there also is a workaround with `eval` that I’ve used before, but I can’t think of that right now.

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [November 20, 2025, 5:35am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/8 "2025-11-20T05:35:35Z")

</div>

> [@JoBe](#):
>
> Nope.
> 
> ```python
> def f():
> a = 1
> print(locals()['a']) # got 1
> 
> exec(f. __code__ , globals(), {})
> f = inspect.getcurrentframe()
> print(f.f_back.f_back.f_locals['a']) # One f_back would be `exec` (would work too)
> 
> ```

Sorry but there’s no way this can work. The frame in which `f. __code__ ` is executed is already gone by the time `exec` returns, so no amount of `f_back` from `inspect.currentframe()` can retrieve what is already lost.

> [@](#):
>
> Apart from that, you could also make `f` return either `a` or even all locals. That would require you to “own” `f` though, so that you can be sure that it always returns what you need.

Yeah I would do that if I owned `f`, which I don’t. I’m trying to make the mechanism work for any given code object.

---

<div class="post-metadata">

**Author:** ![JoBe](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jobe/32/26961_2.png) [@JoBe](https://discuss.python.org/u/JoBe)\
**Post date:** [November 20, 2025, 6:46am UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/9 "2025-11-20T06:46:22Z")

</div>

I’ve tested it, and it seems like you are right. Perhaps a sys hook is the one feasible way.

If I remember correctly, I had something similar, where I had a string that created a function be executed, then grabbing the locals (the function).

Iirc, there I made it so that globals=locals (passing the same dict twice).

I might be able to show the code in a bit, so that might be able to help.

---

<div class="post-metadata">

**Author:** ![blhsing](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/blhsing/32/25812_2.png) [@blhsing](https://discuss.python.org/u/blhsing)\
**Post date:** [December 26, 2025, 2:16pm UTC](https://discuss.python.org/t/is-there-a-good-way-to-get-the-local-namespace-of-a-given-function-derived-code-object-from-exec/104939/10 "2025-12-26T14:16:14Z")

</div>

> [@csm10495](#):
>
> Maybe a flag could be added to allow updating though.

> [@ncoghlan](#):
>
> 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.

Since the current `exec` simply ignores the locals namespace when given an optimized code object, we can implement the default backwards-compatible state of `exec`’s new flag (tentatively named _sync\_fast\_locals_) by setting `locals = globals`, which is what currently happens when locals is not supplied.

When _sync\_fast\_locals_ is enabled, `exec` shall proceed to synchronize the supplied locals to fast locals before executing the code, and synchronize fast locals back to the supplied locals after the execution finishes.

I’ve submitted a draft [PR](https://github.com/python/cpython/pull/143129), which passes all current unit tests plus the following new test:

```
def test_eval_exec_sync_fast_locals(self):
    def func_assign():
        a = 1

    def func_read():
        b = a + 1
        a = 3

    for executor in eval, exec:
        with self.subTest(executor=executor. __name__ ):
            ns = {}
            executor(func_assign. __code__ , {}, ns, sync_fast_locals=True)
            self.assertEqual(ns, {'a': 1})
            ns = {'a': 1}
            executor(func_read. __code__ , {}, ns, sync_fast_locals=True)
            self.assertEqual(ns, {'a': 3, 'b': 2})

```

Question though:

I implemented the sync both ways just for completeness, but while sync’ing from fast locals back to the supplied locals mapping after execution makes a lot of sense as seen in the `func_assign` function, I’m not sure if there’s any good use case for sync’ing from a supplied locals mapping to fast locals before execution, since as you see from the above `func_read` function, it would normally fail with `UnboundLocalError` if called directly because there’s no way to assign a value to the local variable `a` without it being an argument, while `exec` offers to no way to specify an argument for a code object.

One other possible way to test the pre-execution sync is to modify _co\_argcount_ of the code object of a function that does take an argument, though it feels equally contrived:

```python
def func_read(a):
    b = a + 1

ns = {'a': 1}
exec(func_read. __code__.replace(co_argcount=0), {}, ns, sync_fast_locals=True)
assert ns == {'a': 1, 'b': 2} # OK

```

Which test of a pre-execution sync do you think make more sense, if a pre-execution sync makes sense at all?
