A closure is a function that carries a snapshot of the variables it used from the scope where it was defined. That memory is why an inner function can keep a counter running, cache a lookup table, or build a small state machine - all without a class in sight.
What is a closure, in one sentence?
When you define a function inside another function and return it, the inner function keeps a reference to the enclosing scope instead of throwing it away when the outer function returns. The returned function is the closure; the remembered variables are its free variables.
def make_greeter(greeting: str):
"""Return a function that greets people with the given greeting."""
def greet(name: str) -> str:
return f"{greeting}, {name}!"
return greet
hi = make_greeter("Hi")
morning = make_greeter("Good morning")
print(hi("Ada"))
print(morning("Grace"))
print(hi.__name__)
print(len(hi.__closure__))
Two different greeters come out of the same factory, each holding on to its own greeting. They are independent objects: changing one would not affect the other, and the factory's local variable stayed alive after the factory returned.
You can see the machinery through __closure__, a tuple of cells holding the captured values. The count of cells is the count of free variables the function captured - one here.
How does Python decide which variables a closure captures?
Only names that the inner function reads and that are not defined inside it become free variables. Everything else - locals of the inner function, globals, built-ins - is looked up normally at call time. The __code__ object of a closure lists the captured names in co_freevars.
def make_counter(start: int = 0):
"""Return a closure that counts up from start."""
count = start
def increment(step: int = 1) -> int:
nonlocal count
count += step
return count
return increment
counter = make_counter()
print(counter(), counter(), counter())
print(counter.__code__.co_freevars)
The three calls print 1, 2 and 3 because count lives in the closure, not in the call. The nonlocal statement is what allows increment to rebind it - without it, count += step would create a brand new local variable and the counter would always return 1.
Note the difference between rebinding and mutating. nonlocal is only needed when you assign to the captured name. If you instead mutate an object - appending to a list, updating a dict - no keyword is required at all, because you are changing the object, not the name.
def make_accumulator():
"""Return a closure that collects the values it is given."""
values: list[int] = []
def add(value: int) -> list[int]:
values.append(value)
return values
return add
acc = make_accumulator()
print(acc(1))
print(acc(2))
print(acc(1) is acc(2))
The third line prints True: both calls return the same list object, so the earlier values are still there. Shared mutable state inside a closure is a legitimate technique and a common source of bugs - when two callers hold the same closure, they share the state with it.
Closures also keep their captured objects alive. If a closure captures a large data structure, that structure cannot be garbage collected while the closure exists, even if nothing else refers to it. In a long-running server this is how memory quietly grows: a callback stored on a long-lived object keeps its whole captured world alive. When a closure feels suspiciously heavy, inspect what it captured with func.__closure__ and check the sizes of the objects in those cells.
Why do all the lambdas in a loop return the same value?
This is the classic closure bug. A lambda captures the variable, not the value that variable happened to hold when the lambda was created. Every lambda in the list shares one variable, so by the time you call them, the loop has finished and the variable holds its final value.
handlers = [lambda: name for name in ("ada", "grace", "linus")]
print([h() for h in handlers])
fixed = [lambda name=name: name for name in ("ada", "grace", "linus")]
print([h() for h in fixed])
The first list prints three copies of linus; the second prints ada, grace, linus. The only difference is the default argument, which binds the current value to a new name at the moment the lambda is created. A closure-based equivalent is a factory function:
def make_reader(name: str):
"""Return a function that reports the captured name."""
return lambda: name
print([reader() for reader in map(make_reader, ("ada", "grace", "linus"))])
The same trap appears in callbacks registered inside a loop, in GUI handlers, and in asyncio tasks created in a loop. Whenever a callback outlives the iteration that created it, bind the value explicitly.
Should you use a closure, a class or a partial?
| Tool | Best when | Cost |
|---|---|---|
| Closure | You need a small piece of state plus a behaviour, and the state is private. | State is invisible at the call site; a debugger shows it as a cell variable. |
| Class | You need several related pieces of state, comparison, or a public API. | More boilerplate, but everything is visible and testable. |
functools.partial | You only need to fix arguments, with no extra behaviour. | Only fixes arguments; it cannot add logic. |
| Class attribute or default argument | Never for mutable state. | Shared across all calls - the classic mutable-default bug. |
Closures and classes are not rivals. A closure is often the right way to write a class with one method and one piece of state, and plenty of production code does exactly that.
Closures also show up in decorators without you noticing. Every decorator from the previous lesson - @announce, @repeat(3) - is a closure that captured the function it wraps. Once you recognise the shape, a whole family of Python libraries stops looking mysterious: callbacks that remember their configuration, validators that remember their rules, and factories that remember their settings.
What to remember
- A closure remembers the enclosing scope's variables, not a copy of their values.
- Use
nonlocalwhen the inner function rebinds a captured name; mutation alone needs nothing. - Bind loop values explicitly with a default argument or a factory function.
- Closures keep state private; classes keep it visible. Pick based on how much state there is.
func.__closure__andfunc.__code__.co_freevarsshow you exactly what a function captured.
Try it yourself: Build a make_rate_limiter(calls_per_second) factory that returns a function which refuses to run more often than the limit, using a closure to remember the last call time. Give it a time.sleep of one second between two calls and check that the second call is delayed. Then convert the same idea into a class with a __call__ method, and compare which version you would rather test.