skip to content

What does Python's locals() return inside a function, and does writing to it change a local?

level: middleimportance: nice to knowfreq 24%

answer

  1. Two functions, two different storage models
  2. One is live, one is copied
  3. Function locals are not a dictionary
  4. A snapshot per call since 3.13
  5. PEP 667 made the semantics explicit

basics

~20 s

Inside a function, locals() returns a plain dict snapshot of the current bindings — since Python 3.13 a fresh one per call. Writing into it never changes the variables. globals() is the module's live namespace dict.

solid answer

~50 s

In a function, the compiler stores locals in a fixed-size array on the frame, so there is no dictionary to hand back. `locals()` therefore builds one: a snapshot of the current bindings. Since **Python 3.13** (PEP 667) each call returns a new, independent dict, so mutating the result is guaranteed to be a dead end — it never writes back to the variables, and a later `locals()` call does not see your edit. Before 3.13 CPython cached one dict on the frame and refreshed it per call, so mutations could appear to persist briefly, which made buggy code look like it worked. `globals()` is the opposite: it returns the module's actual namespace dictionary, so writing into it really does create or rebind a module-level name. At module level `locals()` and `globals()` are the same object.

code

pycon · 8 lines
pycon
>>> def f():
...     x = 1
...     snap = locals()
...     snap["x"] = 99
...     return x, snap["x"], locals() is snap
...
>>> f()
(1, 99, False)

go deeper

for a junior

Recall that locals() and globals() show you the current names, and that you read them rather than write through them. Predicting that a variable is unchanged after editing the locals() result is enough here.

for a middle

Explain the storage difference — module namespace is a real dict, function locals are array slots on the frame — and name the 3.13 change that made locals() an independent snapshot per call.

for a senior

Show judgement about dynamic namespace manipulation in production code: globals() writes really work and really defeat static tooling, so be able to argue for an explicit registry instead and to spot the pattern in review.

for a principal

Own the policy angle — dynamically defined module names and introspection-based wiring trade a little convenience for a codebase no tool can analyse. Decide where that is acceptable and what the supported alternative in your platform is.

## Why the two functions differ so much `globals()` and `locals()` sound symmetric and are not, because the namespaces behind them are stored in completely different ways. A module's namespace genuinely *is* a dictionary — the one attached to the module object — so `globals()` can return it directly. It is live: put a key in it and you have created a module-level name; rebind a key and every later bare-name read of it in that module sees the new value. A function's locals are not a dictionary. While compiling the function the interpreter fixes the set of local names and assigns each a slot in an array on the frame, which is what makes local access an array index rather than a hash lookup. There is nothing dictionary-shaped to return, so `locals()` materialises one by copying the slots into a fresh mapping. ## What changed in 3.13 Historically CPython kept one dictionary on the frame and re-populated it on each `locals()` call. That produced genuinely confusing behaviour: mutations to the returned dict survived until something refreshed it, and whether they survived depended on tracing, debuggers, and which build you were on. **PEP 667, shipped in Python 3.13**, made the semantics explicit: in an optimised (function) scope, `locals()` now returns an independent snapshot on every call. Two calls return two different dicts, and neither is connected to the running variables. Debuggers keep the ability to *write* locals through a separate write-through view of the frame, which is the supported mechanism — `locals()` is not. ```pycon >>> def f(): ... x = 1 ... snap = locals() ... snap["x"] = 99 ... return x, snap["x"], locals() is snap ... >>> f() (1, 99, False) ``` The local `x` is still `1`, the snapshot says `99`, and the second call produced a different object. ## The practical rule Treat `locals()` in a function as read-only introspection. It is fine for logging — dumping the arguments and working values of a failing call — and fine for formatting, as in building a message from a mapping of current values. It is not a mechanism for creating variables dynamically. Code that tries `locals()[name] = value` to synthesise a local silently does nothing; the honest alternatives are a dictionary you own, `setattr` on an object, or a small class. `globals()` is a real write path, and that is precisely why it deserves suspicion. `globals()[name] = value` genuinely defines a module-level name, so it is occasionally used by registration and plugin code, but it defeats every static tool: linters, type checkers, IDE navigation and readers all lose track of where the name came from. Prefer an explicit registry dictionary, and reach for `globals()` only where the dynamic definition is the point. ## The other two scopes At module level, `locals()` and `globals()` return the same object — `locals() is globals()` is `True` — because the module's local namespace *is* its global namespace. In a class body, `locals()` gives the class namespace as it is being built, which is how some metaclass and DSL tricks read what has been defined so far. In a comprehension or a generator expression the result reflects that construct's own implicit scope, not the surrounding function's. ## Interview shape The question is usually posed as a snippet: assign to `locals()` and ask what the variable is afterwards. The answer is that the variable is unchanged, and the reason is the storage model — the array of slots on the frame is the source of truth and the dict is a copy of it. Naming PEP 667 and the 3.13 change is what separates a memorised gotcha from an understood one, because it explains why older answers on the internet disagree with what the reader observes today. Being able to contrast that with `globals()`, which really is live, shows you understand the mechanism rather than the rule.

  • Can you create a module-level name by writing into the mapping globals() returns?
    Yes — that mapping is the module's live namespace, so `globals()["NAME"] = value` really defines a module global that later bare-name reads in that module will find. It works, but it hides the definition from linters, type checkers and readers, so an explicit registry dictionary is almost always the better design.
  • Why can't the interpreter make writes through the locals() mapping work in a function?
    Because the function's variables live in a fixed-size array on the frame, chosen while compiling the function, and the mapping is a copy of that array. Making arbitrary dict writes flow back would mean giving up the fast slot-based storage or maintaining a proxy on every call. PEP 667 chose to make the copy semantics explicit instead.
  • What does locals() return at module level and inside a class body?
    At module level it returns the same dictionary object as `globals()`, since the module's local and global namespaces are one namespace. Inside a class body it returns the namespace being built for the class, reflecting what has been defined so far — which is how some metaclass and mini-DSL code inspects the body mid-definition.

saying these in an interview costs you the question

  • Assigning into locals() creates or changes a local
  • Says locals() returns the same object each call
  • Treats globals() as a read-only copy too
  • Thinks locals() and globals() differ only by scope name
  • Uses locals() to define variables dynamically

context