Understanding the Python locals() Function
python locals function: Explains what Python's locals() returns, how scope changes its behavior, why writes to it fail, and when to use it in real code.
The python locals function — locals() in code — returns a dictionary that represents the current local symbol table: the names of local variables mapped to their current values. It is one of the built-in introspection tools Python exposes, and it behaves differently depending on where it is called. Understanding those differences matters because the common assumption — that locals() gives you a live, writable view of local variables — is wrong in CPython.
What locals() Actually Returns
When called inside a function, locals() builds a dictionary from the local variables that exist at that point in execution.
def example(): name = "ada" count = 3 print(locals()) example()
The output is {'name': 'ada', 'count': 3}. The dictionary contains only the names that are currently bound in the local scope. If a variable is defined after the call, it will not appear in the result from the earlier call.
The keys are always strings, and the values are the current object references. This makes locals() convenient for inspection, but it is not a live view.
How Scope Changes the Result
The behavior of locals() depends on the scope in which it is evaluated.
At module level, locals() returns the same dictionary as globals():
# module level print(locals() is globals()) # True
Inside a class body, locals() returns the namespace that is being constructed for the class. This is why you will sometimes see locals() used inside class definitions to build a dictionary of class attributes.
Inside a function, locals() returns a separate dictionary that reflects the function's local variables at the moment of the call. The important distinction is that at module level the returned dictionary is the actual namespace, while inside a function it is a snapshot.
Why Modifying the Returned Dictionary Fails
The most common mistake with locals() is trying to use it to set local variables:
def broken(): x = 10 locals()['x'] = 99 print(x) # still 10 broken()
The assignment to locals()['x'] does not change the local variable x. In CPython, local variables in a function are stored in an array of slots on the frame, not in a dictionary. The locals() call builds a new dictionary from those slots each time it is invoked. Writing to that dictionary has no effect on the underlying frame.
This is a CPython implementation detail, but it is the behavior that virtually all Python developers will encounter. The language specification does not guarantee that locals() returns a live mapping, and code that relies on writing to it is not portable.
Practical Uses for locals()
The main legitimate use of locals() is read-only introspection. It is useful for logging the current state of a function for debugging, building a context dictionary for a template engine, or passing the current scope to a helper that needs to inspect names.
def process_order(order_id): status = "pending" attempts = 0 logger.debug("order state: %s", locals())
Because locals() captures the current values, it is a quick way to snapshot the state of a function without listing each variable manually.
Closures and Nested Functions
When a function contains a nested function, locals() does not include free variables that are captured by the inner function. Consider:
def outer(): value = 42 def inner(): return value print(locals()) # includes 'value' and 'inner'
Here value appears in outer's locals because it is a local variable of outer. But inside inner, calling locals() will not show value because value is a free variable, not a local of inner. This distinction matters when you use locals() to build a context for a nested callback or decorator: the captured variables from the enclosing scope are not part of the inner function's local symbol table.
Performance Cost of Calling locals()
Each call to locals() inside a function allocates a new dictionary and copies the current local variable bindings into it. In a tight loop, this allocation happens on every iteration:
def hot_loop(): total = 0 for i in range(1000): total += i # avoid doing this in the loop _ = locals() return total
The cost is proportional to the number of local variables in the frame. For a function with a handful of locals, the overhead is small but not free. If locals() is called thousands of times per request, the dictionary allocations add up. Prefer calling it once at the end of a function, or avoid it entirely in performance-sensitive paths.
Alternatives: vars(), globals(), and inspect
vars() is closely related. Without an argument, vars() behaves like locals() at module level, but inside a function it raises a TypeError because functions do not have a __dict__ attribute. With an object argument, vars(obj) returns obj.__dict__.
globals() returns the module-level namespace and is always a live dictionary. Writing to globals() does affect the module, which is why it is sometimes used for dynamic configuration, though it is rarely a good idea.
For deeper frame inspection, the inspect module provides inspect.currentframe() and inspect.getargvalues(), which give access to the actual frame object. These are useful in debuggers and profilers but carry more overhead and are not intended for application code.
A Template Context Pattern and Its Pitfall
A common pattern is to build a template context from locals():
def render_user(user, items): title = "User page" context = locals() return template.render(**context)
This works as long as every variable that the template needs is already bound before the locals() call. If you add a variable after the call, it will not be in the context:
def render_user(user, items): context = locals() title = "User page" # too late return template.render(**context)
The title variable is missing from context because locals() was called before title was assigned. The same problem appears if a variable is rebound after the call. This makes locals() a fragile way to build contexts: the order of statements changes the result. An explicit dictionary or keyword arguments are more predictable and easier to maintain.