In Python, what do exec()'s globals and locals mapping arguments control, and where does a name bound by the executed code end up?
answer
- Two namespaces, not two options
- Reads fall through, writes do not
- Builtins arrive uninvited
- Functions defined inside see globals only
- A function's locals are a snapshot
basics
~20 sThey are the namespaces the executed code reads and writes. Reads fall back locals to globals to builtins; new bindings land in the locals mapping. Omit both and the code uses the caller's namespaces instead.
solid answer
~50 s`exec(source, globals, locals)` runs the code as if it were module-level code inside the namespaces you hand it. Name lookups walk the locals mapping, then the globals mapping, then builtins; every new binding - a variable, a `def`, a `class`, an `import` - goes into the **locals** mapping. Pass only `globals` and it is used for both, so bindings appear there. Pass neither and the code runs against the caller's own globals and locals. `globals` must be a real `dict`; `locals` may be any mapping object. If the `globals` dict has no `"__builtins__"` key, Python inserts a reference to the builtins module before running, so builtin names work by default. The classic trap is calling `exec()` inside a function: it writes into a snapshot of that function's locals, so it cannot rebind the function's own variables.
code
python · 4 linesg = {"rate": 3}
l = {}
exec("fee = rate * 4", g, l)
print(l["fee"], "fee" in g)go deeper
Know that exec() takes two optional mapping arguments and that leaving them out means the code runs in your own namespaces. Remember that names the code creates do not magically appear as local variables you can use afterwards.
Explain the lookup order - locals, then globals, then builtins - and that all new bindings land in the locals mapping. Be ready to say why exec() inside a function cannot change that function's variables, and that globals must be a real dict.
Demonstrate the read-results-back-from-a-dict pattern for generated code, and diagnose the classic bug where a function defined inside the executed source cannot see names bound alongside it. Be clear that these mappings are not an authority boundary.
Own the guidance for the codebase: where generated-and-executed source is permitted, that its inputs must be values your code produced, and that namespace arguments are documented as hygiene rather than as security in any review you sign off.
## The two mappings are namespaces, not options `exec()` runs its source the way a module body runs: with a globals namespace and a locals namespace. The signature is `exec(source, globals=None, locals=None)`, and those two arguments *are* the namespaces the code lives in for the duration of the call. There are three shapes: - **Both omitted.** The code runs in the caller's own globals and locals. At module level that means it genuinely rebinds module-level names. - **Only `globals` given.** That single dict is used for both roles, so lookups and bindings all happen in it. This is the shape you want when the executed code defines a function that must later see names the same call created. - **Both given.** Reads consult `locals` first, then `globals`, then builtins; every binding goes into `locals`. `globals` and `locals` stay separate, and that separation causes the surprises below. `globals` must be a real `dict` - the interpreter indexes it directly and will raise `TypeError` for anything else. `locals` is looser: any object implementing the mapping protocol will do, which is what makes tricks like a mapping that logs or lazily supplies names possible. ## Builtins are injected unless you say otherwise If the `globals` dict you pass has no `"__builtins__"` key, Python inserts one pointing at the builtins module before executing. That is why `exec("print(1)", {})` works even though you passed an empty dict. Supplying `{"__builtins__": {}}` suppresses that injection, which removes the convenient names - and is routinely mistaken for a sandbox, which it is not. ## Where a binding lands ```python g = {"rate": 3} l = {} exec("fee = rate * 4", g, l) ``` After this, `l["fee"]` is `12` and `g` is unchanged apart from the injected builtins key. The read of `rate` missed `l`, fell through to `g` and succeeded; the write went to `l`. Code compiled in exec mode uses name-style lookups precisely because a module body's namespace can change under it, which is what makes this fall-through behaviour possible at all. The consequence people hit in real code: a function *defined* inside the executed source closes over the **globals** mapping, not the locals mapping. So if the source is `"helper = lambda: rate * 4"` with separate mappings, `helper` lands in `l`, but when you later call it, its global lookup for `rate` goes to `g`. Names that the exec'd source bound in `l` are invisible to it. The same applies to a `class` body and to comprehensions. The fix is almost always to pass one mapping for both arguments. ## Why exec() cannot rebind a function's locals ```python def f(): x = 1 exec("x = 99") return x # still 1 ``` Inside a function, the compiler resolves local variables to fixed slots in the frame rather than to dictionary lookups. When `exec()` is called with no mapping arguments there, the locals namespace it receives is the *snapshot* that `locals()` produces for an optimized scope - a plain dict copied from those slots. The executed code writes `x` into that snapshot, the snapshot is discarded, and the real slot is untouched. **PEP 667, in Python 3.13**, formalized this: `locals()` in a function is specified to return an independent snapshot, so the old "sometimes it seemed to work" folklore is now definitively answered - it does not. (3.13 also made a frame's `f_locals` a write-through proxy, reachable via `sys._getframe()`, but that is a debugger facility, not something exec() routes through.) The practical rule: if executed code must produce values you can use, give it an explicit dict and read the results out of it. Never rely on it mutating the enclosing function. ## Reading results back out The idiomatic pattern is to treat the locals mapping as the return channel: ```python ns = {} exec(source, {"__builtins__": __builtins__}, ns) result = ns["result"] ``` This is also why generated-code helpers in libraries usually execute into a fresh dict and then pull the one function they built out of it by name. ## The security caveat that belongs with the mechanics It is tempting to read the mapping arguments as an access-control mechanism: "the code can only see what I put in the dict". They are not. They control *name resolution*, which is a convenience boundary, not an authority boundary - every object you place in either mapping is itself a starting point the executed code can work from. Use the mappings to keep executed code from stomping on your module state, not to make hostile input safe.
- Why can a function defined inside exec'd code not see the names bound in the locals mapping you passed?Because a function closes over the globals mapping of the code that defined it, not over the locals mapping. Its later global lookups go to the `globals` dict, while the exec'd bindings went into `locals`. Passing the same mapping for both arguments makes the two views agree, which is why single-mapping calls are the norm for generated code.
- Does exec() accept any mapping for both namespace arguments?No. `globals` must be a real `dict`, because the interpreter indexes it directly; anything else raises `TypeError`. The `locals` argument may be an arbitrary mapping object, which is what lets people pass a custom mapping that supplies names lazily or records which names the executed code touched.
- How do you get a value out of exec() when it always returns None?Execute into a dict you own and read the name back: pass a fresh mapping as `locals`, have the source bind a known name such as `result`, then index the mapping afterwards. Library code that generates functions does exactly this - execute into an empty namespace, then pull the single defined function out by name.
saying these in an interview costs you the question
- Thinks exec() with mappings sandboxes the executed code
- Expects exec() to rebind a function's local variables
- Says an empty globals dict removes builtin names
- Believes new bindings go into the globals mapping
- Passes a non-dict as the globals argument
- Assumes a function defined inside sees the locals mapping