Closures in Python

Learn what closures are in Python, how they capture variables from enclosing scopes, and how to use them for function factories, callbacks, and state encapsulation.

7 min read

The article on nested functions in Python introduced the concept of defining functions inside other functions and showed how inner functions can access variables from the enclosing scope. A closure is what happens when a nested function outlives its enclosing scope, when the outer function returns and the inner function survives, carrying its captured variables with it. That inner function, along with the variables it remembers from its birthplace, is a closure.

Closures are not a separate language feature with their own syntax. They emerge naturally from the combination of nested functions and Python's scoping rules. When an inner function references a variable from its enclosing scope, Python stores a reference to that variable inside the function object. As long as the function object exists, the variable it references is kept alive by the closure, even after the outer function has finished executing and its local scope has been dismantled. This is what makes closures powerful: they carry state without requiring classes, global variables, or mutable default arguments.

How closures capture variables

When Python compiles a nested function that accesses a variable from an enclosing scope, it records that variable as a free variable. Free variables are stored in the function object's closure attribute as a tuple of cell objects, each cell containing a reference to the variable's current value. You rarely need to inspect closure directly, but understanding that captured variables are stored as references, not as snapshots, explains the closure behaviors that surprise beginners:

pythonpython
def make_greeting(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet
 
hello = make_greeting("Hello")
hi = make_greeting("Hi")
 
print(hello("Ada"))  # "Hello, Ada!"
print(hi("Ada"))     # "Hi, Ada!"

The inner greet function references greeting from its enclosing scope. When make_greeting("Hello") returns, the returned greet function carries a closure containing the string "Hello". When make_greeting("Hi") returns, the second greet function carries a closure containing "Hi". The two returned functions are independent; each has its own closure with its own captured greeting value. This independence is what makes the factory pattern work: each call to the outer function creates a fresh enclosing scope and a fresh set of captured variables.

The variable is captured by reference, not by value. If the captured variable is mutable and something modifies it, the closure sees the modification. This is the same pass-by-assignment behavior covered in the article on passing objects to Python functions. A closure that captures a list will see changes made to that list, whether those changes come from inside the closure or from other code that holds a reference to the same list.

The late binding gotcha

The most common closure bug occurs when closures are created in a loop and all of them capture the same loop variable. Because the variable is captured by reference, all closures see its final value after the loop completes, not the value it had when each closure was created:

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

All three lambdas capture the variable i, not its value at each iteration. When the lambdas are called after the loop, i is 2, so all three return 2. This is late binding: the variable lookup happens when the function is called, not when it is defined. The fix is to bind the current value as a default argument, because default argument values are evaluated at definition time:

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

The expression lambda i=i: i binds the current value of i to the parameter i of the lambda. Each lambda gets its own default value for i, evaluated at the moment the lambda is created. When the lambda is called without arguments, the default value fills in, and the lambda returns the value from its own creation time rather than the loop variable's final value. This pattern works identically for def functions created in loops.

The late binding behavior is not a bug. It is the consistent application of Python's scoping rules: variables are looked up at call time, and closures hold references to variables, not snapshots of their values. The same behavior applies to nested def functions as to lambdas. Understanding this consistency is more valuable than memorizing which specific situations trigger surprising results.

Practical uses of closures

Function factories are the most common use of closures. An outer function accepts configuration, and the inner function it returns uses that configuration to perform its task. The factory pattern is cleaner than a class with a single method because it avoids the boilerplate of init, self, and class definition, and the captured configuration is truly private because no attribute access can reach into the closure from outside.

Callbacks and event handlers also benefit from closures. A function that registers a callback for a specific event can capture context from the registration site in a closure, so the callback knows which context it belongs to when it fires. This pattern appears in GUI programming, asynchronous task scheduling, and any situation where a function needs to be called later with some context from the moment it was created.

Lightweight state encapsulation is another closure use case. The counter example from the nested functions article, where make_counter returns an increment function that keeps private count, is a closure. The count is modifiable through the returned function but inaccessible from any other code. This is a minimalist form of object-oriented programming: a function that carries private state and exposes behavior through its return value. When the state is simple and the behavior is a single operation, a closure is often more concise and readable than an equivalent class.

Closures and memory

Because closures keep references to their captured variables, those variables are not garbage-collected as long as the closure exists. If a closure captures a large data structure and the closure persists for the lifetime of the program, that data structure also persists. This is usually the intended behavior, the captured data is what the closure needs to function, but it is worth being aware of when closures capture objects that are expensive in memory.

A closure that captures a loop variable and the loop iterates over a large dataset does not capture the entire dataset, only the specific value that the variable held when the closure was created. Python's garbage collector frees objects that are no longer referenced, and closures only hold references to the specific variables they access, not to the entire enclosing scope. This means closures are memory-efficient in practice, and worrying about accidental memory leaks from closures is rarely necessary in typical Python code.

Rune AI

Rune AI

Key Insights

  • A closure is a nested function that retains access to variables from its enclosing scope after the outer function returns.
  • Closures capture variables by reference, not by value; this leads to the late binding gotcha in loops.
  • Use closures for function factories, callback registration, and lightweight state encapsulation.
  • Fix late binding by binding values as default arguments: lambda x=x: ... for lambdas.
  • Each call to the outer function creates a new closure with its own set of captured variables.
RunePowered by Rune AI

Frequently Asked Questions

What is a closure in Python?

A closure is a function object that remembers values from its enclosing scope even after that scope has finished executing. In Python, closures are created when a nested function references variables from its outer function and the outer function returns the nested function. The returned function carries those captured variables with it as part of its closure.

What is the difference between a nested function and a closure?

Every closure is a nested function, but not every nested function is a closure. A nested function becomes a closure when it references variables from its enclosing scope and is returned or passed outside that scope. A nested function that only uses its own parameters and local variables, without accessing enclosing variables, is not a closure.

How do I fix the late binding issue with closures created in a loop?

The late binding issue occurs because closures capture variables, not their values at creation time. When closures created in a loop all reference the same loop variable, they all see its final value. Fix this by binding the current value as a default argument: instead of lambda: i, write lambda i=i: i. Default argument values are evaluated at definition time, so each closure captures the value from its own iteration.

Conclusion

Closures are one of Python's most elegant mechanisms for creating functions that carry private state. They power function factories, callback handlers, and lightweight encapsulation patterns that would otherwise require classes. Understanding how closures capture variables, and the late binding behavior that trips up beginners, gives you a tool that sits between simple functions and full classes in Python's spectrum of abstraction.