A closure is a function associated with enclosing lexical bindings; it can read or update them even after the call that defined it has returned.
A closure retains variable bindings, not values frozen when the function is created, so callbacks built in a loop may all read the final iteration value.
Use nonlocal to rebind enclosing state; when each callback needs an independent value or state, bind an argument at creation or call a factory per iteration.
What it is and why it exists
A closure is a function object together with access to the enclosing bindings it needs from its definition site. In Python, the usual shape is an outer function that creates and returns an inner function. The outer call can return while the inner function keeps using bindings created by that particular call.
A name used by an inner function but not bound in its own code block is a free variable . The lexical scope that supplies the binding is an enclosing scope . The function’s code and access to those bindings provide the complete context needed when it runs.
Closures let lexical scope keep working when a function is passed around as a value. They suit callbacks that carry a small amount of configuration or a narrow function interface over state, without a module global. You encounter them in function factories, decorators, validators, event handlers, and key functions passed to sorted().
Returning the inner function isn’t a semantic requirement for a closure; it merely makes the closure’s lifetime easy to observe. A nested function that refers to an enclosing function binding is still a closure when called before the outer call returns. Conversely, a nested function that uses only parameters, locals, and globals has None in __closure__.
A closure fits one main operation and a small amount of plainly named state. If callers need several public operations, validation rules, a serializable representation, or inheritance, a class usually states the contract more directly. Choose from the interface and ownership model, not a blanket claim about speed or memory.
How it works
Python resolves names from source-code nesting, not from whichever function called the current one. When the compiler finds inner code that refers to a name bound in an outer function, it classifies that name as free in the inner code and arranges shared storage. A same-named local elsewhere on the call stack doesn’t affect that choice.
Each execution of the outer function creates bindings for that call. Creating the inner function gives the function object a route to the bindings it needs; returning the function doesn’t copy their current objects into a snapshot. A later call reads the objects those bindings refer to at that time, so rebinding changes what the closure subsequently sees.
You can follow closure creation and use in this order:
- Call the outer function and create local bindings for that invocation.
- Execute the inner function definition and connect the enclosing bindings it actually uses.
- Pass the inner function out of the outer call, or use it within that call.
- After the outer call ends, bindings still referenced by the closure remain alive.
- Call the closure and read or update those same bindings.
A closure doesn’t automatically retain the outer function’s entire local namespace. Only bindings that the inner code actually uses as free variables need to remain. One such binding can still refer to a large mutable object or object graph, so “only one captured name” says little about retained data size.
Reading, mutation, and rebinding
Reading a free variable needs no declaration. Mutating the mutable object referenced by a free variable also needs no declaration because the binding itself doesn’t change. nonlocal is needed when the inner function makes the name refer to another object.
| Inner operation | What happens | Needs nonlocal |
|---|---|---|
return count | Reads an enclosing binding | No |
items.append(value) | Mutates the bound list | No |
count += 1 | Reads and then rebinds the name | Yes |
items = [] | Rebinds the name to a new list | Yes |
count += 1 is easy to misread. Integers can’t be mutated in place, so the statement reads the old integer, computes a new one, and assigns it to count. Any assignment in the function body makes count local there by default; without nonlocal count, reading that not-yet-assigned local raises UnboundLocalError.
The target of nonlocal
nonlocal name targets the nearest enclosing function scope where the name is already bound. It doesn’t create an outer binding and can’t target the module-global scope. If the compiler finds no suitable binding, it raises SyntaxError before the code runs.
global and nonlocal solve different problems. global directs assignment to the module namespace. Mechanically replacing nonlocal with global in generated code turns state owned by one factory invocation into state shared across the module.
Several functions created by the same outer invocation can share a binding; functions created by separate invocations usually get separate bindings. “Built by the same factory” therefore doesn’t mean shared state. The relevant questions are which invocation created each function and which binding it uses.
Examples
These four examples begin with read-only configuration, then add mutable state, shared operations, and loop-created callbacks. Every shown output comes from running the corresponding file with local python3.
Carrying read-only configuration
The label factory configures a prefix once and returns a function that takes only an order number. It only reads prefix, so it doesn’t need nonlocal.
def make_labeler(prefix):
def label(order_id):
return f"{prefix}-{order_id:04d}"
return label
invoice_label = make_labeler("INV")
return_label = make_labeler("RET")
print(invoice_label(7))
print(return_label(7))
print(invoice_label.__code__.co_freevars)
print(invoice_label.__closure__[0].cell_contents)INV-0007
RET-0007
('prefix',)
INVThe two make_labeler() calls create separate prefix bindings, so the label functions don’t affect each other. co_freevars names the free variables, and cells at the matching positions in __closure__ hold their bindings. Positional inspection is useful for diagnosis, not as an application interface.
The string object happens to be immutable, but the closure hasn’t copied it. The function still reaches "INV" through its own binding. If another closure later rebinds that same name, a reader sharing the binding sees the new object.
Keeping state with nonlocal
The counter must make count refer to a new integer, so record() declares nonlocal count. Each factory invocation produces an independent count.
def make_attempt_counter(start=0):
count = start
def record():
nonlocal count
count += 1
return count
return record
email_attempt = make_attempt_counter()
sms_attempt = make_attempt_counter(10)
print(f"email: {email_attempt()}, {email_attempt()}")
print(f"sms: {sms_attempt()}")
print(f"email: {email_attempt()}")email: 1, 2
sms: 11
email: 3Interleaving the calls shows that the counters don’t leak state. Assigning email_attempt to another name wouldn’t create a new counter because both names would still refer to one function object. Call the factory again when you need independent state.
This interface lets a caller advance the count but not directly set count to an arbitrary value. That is encapsulation by scope, not a security boundary. Code that inspects the function object can still see the cell contents.
Sharing one cell between operations
One make_quota() call returns two functions. reserve() rebinds remaining, while available() reads it; both functions connect to the same cell.
def make_quota(limit):
remaining = limit
def reserve(units):
nonlocal remaining
if units <= 0:
raise ValueError("units must be positive")
if units > remaining:
return False
remaining -= units
return True
def available():
return remaining
return reserve, available
reserve, available = make_quota(5)
print(reserve(2), available())
print(reserve(4), available())
print(reserve(3), available())True 3
False 3
True 0The failed second reservation leaves remaining unchanged, and available() observes that shared state. If more operations accumulate, a positional group of anonymous functions becomes hard to read. A class with reserve() and available() methods is usually clearer at that point.
A shared cell doesn’t provide synchronization. If threads or async tasks can interleave the check and deduction, if units > remaining followed by remaining -= units isn’t one business-atomic operation. Add synchronization suited to the execution model or give the state one owner.
Comparing late binding with creation-time binding
Both lists in the loop create three lambdas. The first group refers directly to the loop variable; the second stores the current object as a default argument.
def make_handlers(queue_names):
late_handlers = []
bound_handlers = []
for queue_name in queue_names:
# These functions share the loop variable's cell.
late_handlers.append(lambda: queue_name)
# The default saves the current object when the function is created.
bound_handlers.append(lambda queue_name=queue_name: queue_name)
return late_handlers, bound_handlers
late, bound = make_handlers(["fast", "bulk", "slow"])
print([handler() for handler in late])
print([handler() for handler in bound])
print(late[0].__closure__[0] is late[1].__closure__[0])
print(bound[0].__closure__)['slow', 'slow', 'slow']
['fast', 'bulk', 'slow']
True
NoneThe first group doesn’t read queue_name until after the loop, when the shared cell points to "slow". This is the late-binding trap. A list comprehension has its own scope, but closures created within one comprehension and called later still share that comprehension variable.
On the second line, the left queue_name is the lambda’s local parameter and the right name is evaluated when the lambda definition runs. The saved object lives in the function defaults rather than a closure cell, so __closure__ is None. When callers shouldn’t be able to override that parameter, a one-call helper factory is often clearer.
Pitfalls
Describing a closure as a value snapshot
Fix: map each free name to its binding and mark when function creation and invocation occur. If you need a snapshot, express that decision with a default argument, a helper factory, or an explicit copy, and state whether the copy is shallow or deep.
Omitting or misusing nonlocal
Fix: put nonlocal near the start of the inner function when it truly rebinds enclosing function state. Don’t add it mechanically for reads or in-place mutation, and use global only for module-owned state. For UnboundLocalError, inspect every assignment in the function, not just the reported line.
Placing the factory call at the wrong lifetime
Fix: decide whether state belongs to the application, queue, request, or individual callback, then place the factory call at that lifetime boundary. Interleave calls to two instances that should be independent and repeatedly call one instance that should preserve state.
Hiding late binding inside a loop
Fix: call all generated functions after the loop and assert each result. A default argument works when you only need to fix one value. When each callback also needs independent mutable state, call a named factory once per iteration.
Retaining an object for too long
Fix: extract the small value the callback needs instead of capturing a broad context for one field. Unregister the callback when its owner finishes. Files, sockets, and transactions need explicit context management rather than waiting for a closure to be collected.
Depending on __closure__ positions
Fix: for diagnosis, pair function.__code__.co_freevars with function.__closure__ by position or use inspect.getclosurevars() to classify bindings. Production code should read and change state through the public function interface, not treat cell layout as a protocol.
Bindings, function objects, and cells
At the language level, the important guarantees are lexical name resolution and continued access to enclosing bindings. CPython implements that shared storage with a cell object . Other Python implementations must preserve observable semantics, but they needn’t use exactly the same bytecode or object layout.
Compile-time name classification
Before a function runs, the compiler classifies names as local, free, cell, or global. An outer function local that inner code references appears in the outer code object’s co_cellvars. For the inner code object, the same name appears in co_freevars.
Classification applies to the whole code block rather than changing along execution paths. Any assignment inside an inner function makes the target local by default unless a global or nonlocal declaration changes the classification. Moving the assignment into if False: therefore doesn’t make an earlier read fall back to the outer binding.
| Observation point | Attribute | What it represents |
|---|---|---|
| Outer code object | co_cellvars | Outer locals also referenced by inner code |
| Inner code object | co_freevars | Names that enclosing scopes must supply |
| Inner function object | __closure__ | Cells corresponding by position to co_freevars |
| Diagnostic helper | inspect.getclosurevars() | Resolved nonlocals, globals, builtins, and unbound names |
A code object describes compiled behavior and may be shared by function objects from several factory calls. __closure__ belongs to a function object and connects it to cells created by a particular execution. Don’t mistake a shared code object for shared runtime state.
Cell identity and sharing
A cell is a small container for the current object reference. When nonlocal rebinds a captured name, the reference inside the cell changes, and every other closure sharing that cell sees the new object on its next read. When code calls append() on a captured list, the cell still refers to the same list, but the list’s contents change.
One outer invocation can create several functions that share a cell, which is why reserve() and available() see one quota. Calling the factory again creates a new outer binding and a new cell, isolating the new quota from the old one. Comparing cell identity can test this relationship, but it shouldn’t drive application behavior.
__closure__ is either None or a tuple of cells. Reading cell_contents can help locate leaked state. Directly changing cell contents couples application code to internals and bypasses validation and invariants that the factory’s interface was meant to enforce.
Default arguments aren’t captured cells
A default expression is evaluated when its def or lambda expression executes, and the result is stored in the function’s defaults. In lambda item=item: item, the left item is a local parameter, so the body doesn’t need to read that name from an outer scope. The idiom solves a timing problem; it doesn’t alter Python’s closure rules.
A default also stores an object reference rather than automatically copying a mutable object. If a loop variable points to a dictionary that is later mutated, each default may hold the dictionary selected in its iteration while calls still observe that dictionary’s newer contents. Make an explicit copy before binding when the contract requires a content snapshot, and choose the required copy depth.
functools.partial() can bind arguments to an existing callable in advance. It returns a partial object, not a Python closure that stores those arguments as free variables. Choose among a default argument, helper factory, and partial() according to the desired signature and readability instead of describing them as one mechanism.
Multiple nesting levels and class scope
With several nested functions, nonlocal selects the nearest enclosing function binding with the same name. It can’t skip that binding to name a farther one, and there is no syntax for selecting a nesting level. If several levels carry same-named state, renaming is often clearer than relying on implicit depth.
An ordinary class namespace isn’t an enclosing function scope for method bodies. If a class body assigns rate = 0.2, a method’s bare rate doesn’t capture it like a nested function would. Access the class attribute through self.rate, type(self).rate, or the class name. Calling both class and function bodies “outer scopes” hides this difference.
A function defined inside a method can capture that method’s local names, including self. As a result, a returned callback may keep the whole instance reachable. When it only needs one immutable instance field, first extract that field into a meaningful local name and then decide whether to capture it.
Diagnosing shared state
When closure state looks wrong, start with source and call lifetimes, then inspect runtime objects. These checks usually locate the problem:
- Use
co_freevarsto list the free names the inner function actually reads. - Pair those names by position with cells in
__closure__. - Check whether two functions reference the same cell, not only whether their
cell_contentscompare equal. - Use
inspect.getclosurevars()to separate nonlocal, global, builtin, and unbound names.
Equal contents don’t prove shared state. Two independent counters can both start at integer 0 while owning different cells; a shared cell can point to different objects at different times. Ownership depends on cell identity and the factory invocation boundary.
Lifetime and execution model
A closure keeps a captured binding alive at least until the last function referring to it becomes unreachable, but exact collection timing still depends on the Python implementation and reference graph. Don’t make correctness depend on finalization timing. Resources that must close promptly need explicit closing, a context manager, or a defined unregister operation.
Closure state has no special concurrency guarantee. Threads, tasks, signal handlers, or reentrant callbacks that reach the same closure can still interleave a check followed by an update. The remedies are the same as for other shared mutable state: narrow ownership, serialize access, or use synchronization designed for the execution model.
Standard pickle locates ordinary functions by a qualified name in a module, and nested local functions usually can’t be imported that way. When work must go to a process pool or cross a process boundary, don’t assume a closure is naturally serializable. A top-level function with explicit data arguments is usually more reliable.
Metadata and wrapper behavior
A closure returned by a decorator is a new function object. Without functools.wraps(), its __name__, documentation, annotations, and __wrapped__ link describe the wrapper rather than the wrapped function. That can confuse tracebacks, API documentation, and tools that inspect signatures.
wraps() copies selected metadata and exposes the wrapped callable for introspection; it doesn’t change which values the wrapper closes over. It also doesn’t isolate state, add synchronization, or make a local wrapper serializable. Those remain separate design decisions.
Type annotations can describe the returned callable’s signature, but ordinary Python execution doesn’t enforce them. For a decorator that preserves an arbitrary callable signature, static typing may need ParamSpec; the runtime closure mechanics are unchanged.
Testing the lifetime contract
Most closure tests should use the public callable rather than __closure__. Exercise a sequence of calls and assert the returned values or visible side effects. That keeps the test valid if the implementation later changes from a closure to a class.
A late-binding test must defer invocation until after the loop or registration phase. An immediate call inside the loop observes the current value and can make broken code look correct. Use distinct iteration values so a duplicated final value can’t hide.
State-isolation tests need at least two factory results. Interleave their calls, then verify that each sequence advances independently. For a deliberately shared pair of operations, test both views after every mutation.
Closure or class
A closure is convenient when the interface has one main call, little state, and no need for public inspection. It also lets a configured function go directly to an API that expects a callable. Well-named factories and returned functions should reveal who owns the state and how many times the factory must be called.
A class fits multiple operations, explicit attributes, protocol implementations, serialization, and state-transition tests. Packing five or six closures into a dictionary or positional tuple usually means you’re manually imitating an object interface. Switching to a class then makes the structure match the public contract.
Further reading
3 questions · 1 predict-the-output · 0 spot-the-bug