Back to Blog
Python

Python Closure Function: Capturing State

python closure function: Learn how Python closure functions capture state from their enclosing scope, with practical examples, common pitfalls, and performance conside...

PythonClosuresNested FunctionsScopingDecoratorsStateful Functions
Diagram of a Python closure function capturing a variable from its enclosing scope

A Python closure function is a nested function that remembers values from its enclosing scope even after that scope has finished executing. This behavior is not a curiosity; it underlies decorators, callbacks, and stateful factories. Consider a simple factory:

def make_multiplier(factor): def multiplier(x): return x * factor return multiplier times_two = make_multiplier(2) print(times_two(5)) # 10

Here, multiplier is a closure because it references factor, a variable from make_multiplier's local scope. When make_multiplier returns, factor is no longer on the call stack, yet times_two(5) still works. The closure captures the variable itself, not just its value at creation time.

How a Nested Function Captures Its Environment

Python resolves names using the LEGB rule: Local, Enclosing, Global, Built-in. When a nested function references a variable from an enclosing function, that variable is called a free variable. The nested function becomes a closure if it references at least one free variable.

def outer(): message = "hello" def inner(): print(message) return inner show = outer() show() # hello

The inner function captures message. Even though outer has returned, message remains accessible because the closure holds a reference to it.

What Python Stores in the Closure Cell

Each free variable is stored in a cell object. The closure's __closure__ attribute is a tuple of these cells. You can inspect it directly:

def outer(): x = 10 def inner(): return x return inner fn = outer() print(fn.__closure__[0].cell_contents) # 10

This cell is shared between the enclosing function and the closure. If the enclosing function mutates the variable after creating the closure, the closure sees the new value. This is the mechanism behind late binding, which we will examine shortly.

Using Closures to Hold Mutable State

Closures can maintain state across calls without using global variables. A counter is a classic example:

def counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment next_value = counter() print(next_value()) # 1 print(next_value()) # 2

The nonlocal keyword is required to rebind count. Without it, Python would treat count as a new local variable inside increment, and the closure would not update the captured cell. This pattern is useful for generators, lazy initialization, and simple state machines.

Closures vs Classes: When to Use Each

Both closures and classes can encapsulate state. The choice depends on complexity and readability.

AspectClosureClass
State storageCell variablesInstance attributes
MethodsNested functionsMethods
Additional dataLimited to captured variablesArbitrary attributes and methods
ReadabilityConcise for single-function logicBetter for multiple related operations

Use a closure when you need a single callable with a small amount of state. Use a class when the state requires multiple methods, property access, or inheritance. A closure is not a substitute for a full abstraction.

The Late Binding Problem in Loops

A common mistake is creating closures inside a loop and expecting each closure to capture the current loop value. Because the loop variable is a single cell, all closures end up seeing the final value.

funcs = [] for i in range(3): funcs.append(lambda: i) for f in funcs: print(f()) # 2 2 2

To capture the value at each iteration, bind it as a default argument or use a factory function that takes the value as a parameter:

funcs = [] for i in range(3): funcs.append(lambda i=i: i) for f in funcs: print(f()) # 0 1 2

The default argument i=i creates a new local variable inside the lambda, breaking the shared cell reference. This is a common source of bugs in GUI callbacks and asynchronous code.

Closures in Decorators and Callbacks

Decorators rely on closures to wrap functions with additional behavior. A simple timing decorator illustrates the pattern:

import time def timer(func): def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.6f}s") return result return wrapper @timer def compute(): return sum(range(1000)) compute()

The wrapper closure captures func and any arguments passed to it. This pattern is also used for memoization, access control, and logging. Callbacks in event-driven systems often use closures to carry context along with the function.

Modifying Captured Variables with nonlocal

When a closure needs to reassign a captured variable, the nonlocal statement is mandatory. Without it, Python creates a new local variable and the closure stops being a closure for that name.

def accumulator(): total = 0 def add(value): nonlocal total total += value return total return add acc = accumulator() print(acc(5)) # 5 print(acc(3)) # 8

If you omit nonlocal, total += value raises an UnboundLocalError because total is treated as local. This is a frequent source of confusion for developers new to closures.

Memory and Lifetime Considerations

A closure keeps its free variables alive as long as the closure object itself is alive. This can lead to unexpected memory retention if a closure is stored in a long-lived container. For example, a list of closures that each capture a large object will keep those objects in memory even after the enclosing function has returned.

def create_closures(): data = [1, 2, 3] # large object return [lambda: data for _ in range(10)] closures = create_closures() # data remains referenced by each closure

If the closures are no longer needed, dropping all references to them allows the captured data to be garbage collected. In long-running applications, be mindful of closures stored in caches or registries. Also note that closures capture variables by reference, not by value, so mutations to mutable objects are visible across all closures sharing that cell.

Understanding how Python closure functions manage state and scope helps you write more predictable code. The same mechanism that powers decorators and callbacks can also introduce subtle bugs if you ignore late binding or forget nonlocal. By applying these patterns deliberately, you can leverage closures for clean, maintainable stateful behavior.

python closure function: Practical Usage and Code Examples | RYUSLOG DEV