Definition
An ordinary function forgets everything between two calls: its local variables vanish as soon as it hands back a result. The annoyance starts when you want to ship a function that is already set up, one that multiplies by two and another by three, without copying the same body twice for a single digit that changes.
A closure is Python's answer to that need. It is a function defined inside another one, which remembers the variables of the enclosing function long after that one has handed back control. The word means what it says: the inner function closes over the context it was born in and carries it along.
Here is the mould, and the two functions that come out of it.
def make_multiplier(factor):
def multiply(number):
return number * factor # factor belongs to the function above
return multiply # the function itself is handed back, not its result
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10)) # 20
print(triple(10)) # 30make_multiplier finished twice, and yet factor is still there, in two independent copies: an ordinary local would have disappeared, this one survives because an inner function still reads it.
What that changes when you write code: you ship a function that is already configured instead of dragging its settings around as an argument everywhere. double is used as a one-parameter function.
The three conditions
A closure is never declared, it is observed: no keyword announces it, the shape of the code is what builds it. That leaves the matter of recognising one. Three elements have to come together, and a single one missing leaves an ordinary nested function.
| Condition | What it means |
|---|---|
| A nested function | A def written inside another one |
| A free variable | The inner function reads a name it does not own and that is not global |
| A way out of the nest | The inner function is handed back by return or stored somewhere |
The third one is the most often forgotten: as long as the inner function stays locked inside the one above, it dies with it and the whole mechanism is pointless.
Python keeps a record of that capture in a __closure__ attribute, sitting on the inner function and holding None when there is nothing to remember. double.__closure__[0].cell_contents returns 2: enough to confirm a closure formed, rather than assuming it did.
It captures the variable, not its value
Here is the trap that costs newcomers the most time. A closure remembers a name and the place that name lives in, not a photograph of its contents: it goes and reads that place when it is called, not a second earlier.
Three functions created in a loop, meant to return 0, 1 and 2.
functions = []
for i in range(3):
functions.append(lambda: i) # i is not read here, only remembered
print([f() for f in functions]) # [2, 2, 2] instead of [0, 1, 2]The three lambda functions share the same scope and therefore the same i, which holds 2 once the loop is over. None of them read it during the loop: they all read it at call time, far too late.
This mistake raises no exception at all. The program runs, the functions answer, and the values are wrong. That silence is what makes it expensive: it surfaces much further along, inside a result that does not add up.
The fix is to freeze the value at creation time with a default argument: it is evaluated once, at definition time, and what gets captured there is a copy, independent of the rest of the loop.
functions = []
for i in range(3):
functions.append(lambda value=i: value) # value=i is evaluated right away
print([f() for f in functions]) # [0, 1, 2]Changing what was captured
Reading a captured variable asks nothing of anyone. Assigning to it is another story: as soon as an inner function writes to a name, Python treats it as local from start to finish, and the call ends in an UnboundLocalError.
The nonlocal keyword removes the ambiguity by pointing explicitly at the variable of the function above. What comes out is a counter that keeps its total from one call to the next.
def counter():
total = 0
def increment():
nonlocal total # total means the one above, not a new local
total += 1
return total
return increment
next_value = counter()
print(next_value(), next_value(), next_value()) # 1 2 3Not to be confused with global, which aims at module level rather than at the function above. Every call to counter builds a brand new counter with its own total, where a global would be shared by the whole program.
Closure or class?
A closure and an object solve the same problem: tying a behaviour to a state that survives between calls. The choice is settled on how much state has to be kept and how many operations have to be offered.
| Criterion | Closure | Class |
|---|---|---|
| Number of operations | One only | Several |
| State to keep | One or two values | As much as needed |
| Readability of the state | Implicit, to be guessed | Explicit, named |
| Writing | Two nested functions | A class and its methods |
The closure wins when there is one thing to do and one or two values to remember. As soon as a third behaviour appears, the class becomes clearer again: the state carries a visible name instead of living hidden in an intermediate scope.
Frequently asked questions
What is a closure good for in real code?
For building functions to measure: a validator set on the length expected, a sorting function configured on the fly, a cache of results. It is also the machinery every decorator rests on, a closure wrapping the function it received.
Does a closure use up memory?
Yes, and that is the point to watch. As long as the inner function exists somewhere, the variables it captured cannot be released: a large table captured in a closure stored in a registry is held for the whole run of the program.
Do closures have to be mastered to write good Python?
They are not essential at the start. What they do explain is a set of things used every day without being understood: decorators, callbacks, function factories. The subject is picked up step by step in the Python course.