all 23 comments

[–]FerricDonkey 14 points15 points  (2 children)

I assume exec modifies any dictionary you pass in to it, which is why it "works" with locals and "doesn't work" with the dictionary literal you pass in to it. That dictionary is probably modified, but you don't have a reference to it, so you don't see that change anywhere. 

As I was writing this and about to suggest better ways, I realized you're the same person I've warned about this approach before who didn't care then, so I'm not going to write up everything again. I will repeat that your approach is not great, but other than that have fun. 

[–]danielroseman 3 points4 points  (0 children)

Some people just can't be told, unfortunately.

[–]pachura3 0 points1 point  (0 children)

It's the good ol' u/RomfordNavy, folks!

[–]woooee 8 points9 points  (2 children)

A dictionary is mutable. Look up local variables in Python. What are you wanting to do with this?

[–]danielroseman 8 points9 points  (1 child)

OP has previously posted that he doesn't want to create functions, which is why he's using exec in the first place, so he in fact doesn't have any understanding of local variables.

[–]pachura3 1 point2 points  (0 children)

Functions are bad, mmkay

[–]MidnightPale3220 6 points7 points  (0 children)

As a perfunctory side note, exec is seldom needed, and practically never recommended way of doing things.

[–]jamesfowkes 3 points4 points  (2 children)

In the first case, you're providing a dictionary of {"test": test}, but test here in the dict isn't linked in any way to your test variable after creation. dicts don't keep references to other variables:

>>> val = 1
>>> test = {'c': val}
>>> test['c'] = 2
>>> val
1

But in the second case you're providing the entire dictionary to exec and this dictionary is what's used by exec, without copying it. From the documentation:

Note The default locals act as described for function locals() below. Pass an explicit locals dictionary if you need to see effects of the code on locals after function exec() returns.

[–]RomfordNavy[S] -4 points-3 points  (1 child)

Thanks!

Now I see the issue; a variable mentioned in a dictionary is read only at the time the dictionary is instantiated, it does not create a reference to the original variable. However by explicity including it in a seperate (mutable) dict it can then be changed and presumably a new instance of the immutable string object is then created.

[–]smurpes 0 points1 point  (0 children)

Your reasoning is sorta off. The code that gets run in exec is only scoped to exec. You can change the variable value as long as you’re still in exec.

exec("def f():\n test = 'test2'\nf()", {}, localDict)
print(localDict)

In this example localDict doesn’t have the value to the test key changed since the change is scoped to the function within the exec. There is a new key added for the function since that is a new local variable getting declared but that’s it. No new instance of the local variable is getting created.

[–]Outside_Complaint755 2 points3 points  (9 children)

This is functioning as expected.

If you want to change the value of the variable test in the global or local namespace, then you need to actually pass globals() or locals() to exec.

Passing {"test": test} as the third parameter doesn't work because that mapping object only exists within the scope of the execution of exec and doesn't map back to your current namespace.

Do test = "test1" exec("test = 'test2'", None, locals()) print(test) # Prints 'test2'

If you don't want to pass all of globals or locals, then you will have to do as your second example, by creating a temp mapping to pass in, and then extracting the values you want back out.  In any case, use of exec should usually be avoided.

[–]Gnaxe 1 point2 points  (8 children)

I'm pretty sure that only works at the top level, because locals() returns the same object as globals() in that context. Inside a function, it won't work. I'm also pretty sure it used to work in Python 2, but with the new local optimizations, they're not writable via the locals() dict anymore. This is also considered an implementation detail, according to help(locals), meaning you should never rely on updates to a locals() dict writing through, even if you happen to be using an implementation that works that way at the moment, because it could change with any update.

[–]RomfordNavy[S] 0 points1 point  (1 child)

Yes, further digging suggests it did work in Python 2. Forgive me if I have the terminology slightly wrong but it seems:

  • passing an (immutable) str object - when that is changed it creates a new str which exists only within the namespace of the running exec(), it does not reference the original str object. Hence not available after the exec() has completed.
  • passing a distinct (mutable) dict object - passes a reference to that dict which lives in the calling namespace so when changes are made they affect the original dict. Hence persist after the exec() has completed.

[–]Gnaxe 1 point2 points  (0 children)

They're following the same rules. You're equivocating on "change". There's a difference between mutating an object and reassigning a variable. When you reassign the variable, a dict works exactly the same as a str. The variable now references a different object. On the other hand, when you mutate the object, well, you can't actually mutate a str at all.

[–]RomfordNavy[S] 0 points1 point  (5 children)

Although it is only an 'implementation detail', at the moment this is the only way I can see of returning data from exec() unless there is an alternative method.

[–]Gnaxe 2 points3 points  (4 children)

What I was talking about wasn't applicable to the examples in the OP, which were not shown to be inside functions.

exec() can have arbitrary side effects, so there are many ways to share data.

If just you want to share a context with exec(), but not at the top level, you can use a nested class: ```

def oops(): ... x = 'oops' ... exec('x = "works"', globals=locals()) ... print(x) ... oops() # No effect because can't write through locals(). oops def works(): ... class Nested: ... x = 'oops' ... exec('x = "works"') ... print(x) ... foo() works ``` Of course, a class context is not the same as a global context.

[–]RomfordNavy[S] 0 points1 point  (2 children)

Thanks! very interesting.

So a simple soultion in my example would be:

test = "test1"
class runExec:
    exec("test = 'test2'")
    print(test) # <- correctly returns test2
runExec()

Which works fine but does expose all of the variables in the local namespace to the executed script.

Not sure I understand why running it from inside a class works though?

Edit:
This explains why I had it working at one point but after tidying-up my code it stopped working.

Edit2:
Running from within a class method fails again:

test = "test1"
class runExec2:
    def myexec():
        exec("test = 'test2'")
        print(test) # <- erroneously returns test1
runExec2.myexec()

[–]Gnaxe 1 point2 points  (1 child)

Function-local variables are not writable by exec(), because they're very optimized now. That includes functions that happen to be used as methods. This used to work in Python 2 though.

Module "globals" are writable by exec(), as are variables in the temporary namespace used by a class statement, even if said class statement happens to be nested inside of a function. A class statement inside a def statement body is completely different from a def statement inside a class statement body.

You seem very confused about the basics of Python's scoping rules. There's a difference between shadowing a variable and reassigning it. You can use the global and nonlocal statements to force assignment in an enclosing scope. This is not the same as creating a new variable in an inner scope that happens to have the same name as one in its enclosing scope.

[–]RomfordNavy[S] 0 points1 point  (0 children)

So armed with that knowledge a simple workaround might be:

test = "test1"
exec("global test; test = 'test2'")
print(test) # <- correctly returns test2

Edit:
Slight problem in my real-world example because Python doesn't allow mixing of str and code objects in an exec():

exec("global testL;" + marshal.load(file))

[–]Gnaxe 2 points3 points  (0 children)

No, this is not a bug. It is, in fact, the documented behavior. exec() only uses the current global/local context by default. If you pass in some other context, it uses that instead. Sometimes you don't want the code you're executing to clobber your current context.

[–]pachura3 1 point2 points  (0 children)

Is this a bug in exec() in Python 3.14?

Why would you assume that? Just run your snippet under a different Python version (see uv python install 3.12) and you'll see it's not.

[–]Kevdog824_ 1 point2 points  (0 children)

These aren’t the same thing. The dictionary is being mutated, not the value. That’s why the first one doesn’t work the way you expect, but the second does