Python Closure Variable Capture Explained
python closure variable: Understand how Python closures capture variables, why late binding surprises many developers, and how to control captured values with nonlocal...
python closure variable requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you define a nested function in Python, it can reference variables from the enclosing function. This behavior is straightforward until you realize that the closure captures the variable itself, not its value at definition time. Consider this classic example:
def make_functions(): funcs = [] for i in range(3): def f(): return i funcs.append(f) return funcs for f in make_functions(): print(f())
This prints 2 three times. Many developers expect 0, 1, 2. The reason is that each f closes over the same variable i, and by the time you call the functions, the loop has finished and i holds its final value. This is the essence of how Python closure variables behave.
What a Closure Actually Captures
A closure in Python is a function object that retains access to variables from its enclosing lexical scope. When the inner function references a variable that is not local to it, Python stores that variable in a special cell object. The closure holds a reference to that cell, not to the value the variable had when the closure was created.
def outer(x): def inner(): return x return inner f = outer(10) print(f()) # 10
Here, inner captures x. Even after outer returns, x remains accessible through f. The variable x lives in a cell that the closure references. This is why closures can outlive their defining function.
The key point is that the cell contains a reference to the current value of the variable. If the variable is reassigned in the outer scope after the closure is created, the closure sees the new value.
The Late Binding Problem
The loop example above demonstrates late binding: the closure looks up the variable at call time, not at definition time. This behavior is not a bug; it is how Python's scoping rules work. The variable i is a single cell shared by all three closures. After the loop completes, i is 2, so every closure returns 2.
To bind the current value at definition time, you can use a default argument:
def make_functions(): funcs = [] for i in range(3): def f(i=i): return i funcs.append(f) return funcs for f in make_functions(): print(f()) # 0, 1, 2
The default argument i=i evaluates the outer i immediately when the function is defined, storing that value as the default. The inner function no longer needs to close over the loop variable.
Using nonlocal to Modify Captured Variables
Closures are not read-only. You can modify a captured variable inside the nested function by declaring it nonlocal. This is essential for building stateful closures like counters:
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter()) # 1 print(counter()) # 2
Without nonlocal, assigning to count inside increment would create a new local variable, breaking the closure. The nonlocal statement tells Python that count refers to the variable in the nearest enclosing scope that is not global.
This pattern is useful for stateful callbacks, lazy initialization, and memoization. However, be cautious: a closure that mutates shared state can make behavior harder to reason about, especially in concurrent code.
Closures and Variable Lifetime
Because a closure holds a reference to a cell, the variables it captures remain alive as long as any closure referencing them exists. This can extend the lifetime of objects that would otherwise be garbage collected.
def outer(): large_data = [i for i in range(1000000)] def inner(): return large_data[0] return inner f = outer() # large_data is still referenced by f's closure
This is often desirable, but it can also lead to memory retention if you keep closures around longer than needed. If a closure captures a large object and you no longer need the closure, make sure to delete it or replace it to allow garbage collection.
Closures vs. Default Arguments
Choosing between a closure and a default argument depends on whether you need to see updates to the variable. If the outer variable changes after the closure is defined, a closure sees the new value; a default argument does not.
| Approach | Captures | Sees later changes | Use case |
|---|---|---|---|
| Closure | Variable cell | Yes | Stateful callbacks, dynamic behavior |
| Default argument | Value at definition | No | Bind a snapshot, avoid late binding |
For example, in a GUI event handler, you might want the current value of a slider at the time the handler is created. A default argument gives you that snapshot. In a counter, you want the current count, so a closure is appropriate.
Memory and Performance Considerations
Each closure adds a small overhead because Python must manage cell objects and indirect references. Creating thousands of closures in a tight loop can be slower than using a class or a simple function with attributes. However, for most applications, the overhead is negligible.
More important is the memory retention issue. If a closure captures a large object, that object stays alive as long as the closure does. This can cause unexpected memory growth if you store many closures in a list or cache. If you need to release the captured variables, you can delete the closure or design it to avoid capturing large objects.
Practical Patterns: Factories and Decorators
Closures are the foundation of decorators. A decorator is a function that takes a function and returns a new function, often using a closure to wrap the original:
def log_calls(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @log_calls def add(a, b): return a + b print(add(2, 3))
Here, wrapper closes over func. The closure retains the original function, and the wrapper can access it whenever it is called. This pattern is pervasive in Python frameworks and libraries.
Another common use is a function factory that creates specialized functions with preconfigured parameters:
def make_multiplier(factor): def multiply(x): return x * factor return multiply times_two = make_multiplier(2) print(times_two(5)) # 10
In this case, factor is captured and remains available for the lifetime of times_two. This is a clean way to create reusable behavior without writing separate functions.
Understanding how Python closure variables work lets you predict when a closure will see updated values, when it will retain memory, and how to control binding with defaults and nonlocal. These details matter in real code, especially when you build factories, decorators, or any function that outlives its defining scope.