How do the globals and locals mappings passed to exec() decide what the code can see?
answer
- Three call shapes, three different meanings
- One dict means globals and locals are the same
- Two mappings resolve like a class body
- Bindings land in the locals mapping
- Nested defs do not close over locals
basics
~20 sThey are the namespace the code runs in. Pass nothing and it uses the caller's scope; pass one dict and it serves as both globals and locals; pass two and lookups fall back locals to globals to builtins, while new names bind into the locals mapping.
solid answer
~50 s`exec()` and `eval()` take up to two namespace arguments and there are three meaningful shapes. **Neither given**: the code runs in the caller's scope, using the caller's globals and its local mapping. **Globals only**: that dict serves as both globals and locals, so the string executes like module-level code and every name it binds appears in your dict. **Both given**: name resolution goes locals, then globals, then builtins, and new bindings land in the locals mapping - the code behaves as if it were a class body. That last shape has a sharp edge: a function or comprehension defined inside the string does not close over the locals mapping, so it raises `NameError` for names that were only in `locals`. `globals` must be a real `dict`; `locals` may be any mapping, so a subclass with a custom `__getitem__` can supply names lazily. Since **3.13** both may be passed as keyword arguments.
code
python · 9 linesns = {"start": 10}
exec("doubled = start * 2", ns)
print(ns["doubled"]) # 20
g = {"start": 10}
l = {}
exec("doubled = start * 2", g, l)
print(l["doubled"]) # 20
print("doubled" in g) # Falsego deeper
Recall the simplest safe pattern: pass one dictionary as the globals argument, and read anything the code bound back out of that same dictionary afterwards. Do not rely on the caller's own scope.
Explain all three shapes and what changes between them: caller's scope, one dict serving as both namespaces, and two mappings with class-body resolution where writes land in locals.
Demonstrate the operational judgement: a fresh mapping per evaluation so nothing leaks between runs or threads, awareness that builtins are injected regardless, and the nested-def trap that makes the two-mapping form surprising.
Own the design question of whether a runtime-evaluated expression surface belongs in the system at all, what its contract is (which names are guaranteed, who authors the strings, how it is reviewed), and what a safer parsed grammar would cost.
## The three call shapes `exec(source, globals=None, locals=None)` - and `eval()` with the same signature - lets you choose the namespace the code runs in. Which of the two arguments you supply changes the semantics, not just the contents. **Neither argument.** The code runs in the caller's scope. At module level that means the module's globals, so `exec("total = 5")` really creates a module attribute. Inside a function it means the caller's globals plus a *snapshot* of the caller's locals, which is why an assignment there never reaches the function's own variables. **Globals only.** The locals argument defaults to the same object, so the string executes in one flat namespace exactly like top-level module code. Everything it binds is visible in your dict afterwards: ```python ns = {"start": 10} exec("doubled = start * 2", ns) ns["doubled"] # 20 ``` This is the shape most code wants. One dict in, one dict out, no surprises about where a name went. **Both arguments.** Now the two namespaces are distinct. Reads resolve locals first, then globals, then builtins. Writes go to the locals mapping only - your globals dict is not modified: ```python g = {"start": 10} l = {} exec("doubled = start * 2", g, l) l["doubled"] # 20 "doubled" in g # False ``` ## Class-body semantics, and the trap that follows The language reference describes the two-mapping form as executing "as if the code were embedded in a class definition", and that is not a loose analogy - it is the same scoping rule. A class body has a local namespace that is a real dict, and crucially **that namespace does not become an enclosing scope for functions defined inside it**. The same holds here: ```python g, l = {}, {"n": 5} exec("def h(): return n\nres = h()", g, l) # NameError: name 'n' is not defined ``` `h` compiles a global lookup for `n`, because there was no enclosing *function* scope to close over. At call time it consults `g`, where `n` does not exist. Comprehensions hit the same wall, since a comprehension body is compiled as its own scope: `exec("out = [n for _ in range(2)]", {}, {"n": 3})` raises `NameError` too. The fix is simply to pass one dict rather than two, which collapses the distinction and makes every name a global from the executed code's point of view. ## What each argument must be `globals` must be an actual `dict`. Handing `exec()` some other mapping raises `TypeError: exec() globals must be a dict`. `locals` is far more permissive: any mapping will do, so a `dict` subclass or a custom class implementing `__getitem__` can compute or log names on demand - a useful hook when the executed code is a small operator-authored formula and you want to resolve field names against a live record. Also worth knowing: if the globals dict you pass has no builtins entry, Python inserts a reference to the `builtins` module into it before running the code. That is a convenience, not a restriction, and it is why an empty globals dict is **not** a sandbox - the executed code still reaches every builtin. ## The 3.13 keyword-argument change Both arguments used to be positional-only. Since **Python 3.13** you can write `eval(src, globals=g, locals=l)`, which mostly helps readability at call sites that pass only the second one - previously you had to pass `None` for globals to reach it. ## Choosing a shape in practice For evaluating a small expression against a record, the sensible default is a single dict per evaluation: build it from the fields the expression is allowed to reference, pass it as globals, and let any bindings land there. Reuse a fresh dict per evaluation rather than a shared one, so no state carries between runs and nothing is shared across threads. Reach for the two-mapping form only when you deliberately want a stable read-only backdrop plus a scratch namespace for results - and remember that a function defined in the string will not see the scratch half.
- Why does a function defined inside the exec'd string fail to see a name from the locals mapping?Because with two mappings the code runs with class-body scoping, and a class body is not an enclosing scope for functions defined inside it. The nested `def` compiles a global lookup for the name, so at call time it consults the globals dict, not the locals mapping, and raises `NameError`. Comprehensions fail the same way, since a comprehension body is its own scope. Passing a single dict avoids the whole issue.
- Does passing an empty dict as globals stop the executed code from reaching builtins?No. If the globals dict has no builtins entry, Python inserts a reference to the `builtins` module before running the code, so `open`, `__import__` and everything else remain reachable. An empty namespace limits which of *your* names the code can see; it does not limit what the code can do. Treating it as a sandbox is a well-known mistake.
- Can the globals argument be a mapping other than a dict?No - `exec()` and `eval()` raise `TypeError` unless globals is a real `dict`, because the interpreter's global-lookup fast paths assume dict layout. The locals argument is unconstrained: any mapping works, including a `dict` subclass or a class implementing `__getitem__`, which is how you supply names lazily or resolve them against a live record.
saying these in an interview costs you the question
- Says an empty globals dict sandboxes the executed code
- Expects new bindings in globals when a separate locals mapping was given
- Thinks omitting both mappings gives a fresh empty namespace
- Assumes a def inside the string closes over the locals mapping
- Passes a non-dict mapping as globals and expects it to work