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:
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:
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:
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
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.
Frequently Asked Questions
What is a closure in Python?
What is the difference between a nested function and a closure?
How do I fix the late binding issue with closures created in a loop?
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.
More in this topic
Define and Call Python Functions
Learn the exact syntax for defining and calling Python functions with the def keyword, including naming rules, the function body, and how the call stack works.
Python Function Parameters and Arguments
Learn the difference between parameters and arguments, how to pass values by position and by keyword, and the rules for default parameter values.
Python Functions Explained
Learn what Python functions are, why they matter for organizing code, and how they form the foundation of non-trivial programs.