Closures
A closure is a function that remembers the variables from the scope it was defined in, even after that scope has finished running.
In Python, functions are first-class objects, and a function defined inside another function can be returned or passed around while still 'remembering' the variables that were alive in its enclosing scope at the time it was created. That remembered environment is a closure. Closures are the mechanism behind decorators, function factories, and callbacks that carry private state without needing a class.
Nested Functions and Enclosing Scope
A nested function is simply a function defined inside another function's body. Python's scoping rules (LEGB: Local, Enclosing, Global, Built-in) mean a nested function can read names from its enclosing function's local scope, in addition to its own locals and the module's globals.
inner() has no local variable named message, so Python looks in the enclosing scope (outer's locals) and finds it there.
def outer():
message = "hello from outer"
def inner():
print(message) # reads 'message' from the enclosing scope
inner()
outer() # hello from outerFree Variables
A name used inside a nested function but defined in an enclosing (not global) scope is called a free variable. When the nested function is returned out of its enclosing function, it takes its free variables with it — this is what turns a nested function into a closure.
make_greeter has already returned by the time greet() is called, yet greet still has access to its own copy of greeting — that's the closure at work.
A closure captures the variable itself, not a snapshot of its value at creation time. If the enclosing variable is later rebound (via nonlocal) before the inner function runs, the inner function sees the new value.
def make_greeter(greeting: str):
def greet(name: str) -> str:
return f"{greeting}, {name}!" # 'greeting' is a free variable
return greet
hello = make_greeter("Hello")
howdy = make_greeter("Howdy")
print(hello("Ana")) # Hello, Ana!
print(howdy("Ana")) # Howdy, Ana!Closure State Persists Across Calls
Each call to the outer function creates a fresh enclosing scope, so each closure it produces has its own independent captured state. This lets a closure act like a small, self-contained object that carries private state between calls without a class.
counter_a and counter_b are two distinct closures over two distinct 'count' variables, even though both came from the same make_counter function.
def make_counter():
count = 0
def increment() -> int:
nonlocal count
count += 1
return count
return increment
counter_a = make_counter()
counter_b = make_counter()
print(counter_a()) # 1
print(counter_a()) # 2
print(counter_b()) # 1 (independent state)The nonlocal Keyword
By default, assigning to a name inside a function makes it a new local variable in that function, shadowing any enclosing variable of the same name — it does not modify the enclosing one. nonlocal name tells Python that assignments to name inside this nested function should rebind the variable in the nearest enclosing function scope instead of creating a new local.
Without nonlocal, Python sees the assignment 'total = ...' anywhere in add_without_nonlocal's body and treats total as local throughout that function, so reading it before assignment fails.
nonlocal only reaches into an enclosing FUNCTION scope, never the global/module scope (that's what the separate global keyword is for). It also requires the name to already exist in an enclosing scope — nonlocal cannot create a new binding there.
def outer():
total = 0
def add_without_nonlocal(x):
total = total + x # UnboundLocalError: 'total' is local here
def add_with_nonlocal(x):
nonlocal total
total += x # rebinds outer's 'total'
add_with_nonlocal(5)
print(total) # 5Mutation Without nonlocal: Mutable Free Variables
nonlocal is only needed to REBIND a free variable (make the name point to a different object). If the free variable refers to a mutable object, you can mutate it in place from the inner function without nonlocal, because that's not an assignment to the name itself.
append() mutates the list object that 'values' already refers to; the name 'values' itself is never reassigned, so ordinary scoping rules allow it.
def make_accumulator():
values = [] # mutable, captured by reference
def add(x):
values.append(x) # mutation, not rebinding — no nonlocal needed
return list(values)
return add
acc = make_accumulator()
print(acc(1)) # [1]
print(acc(2)) # [1, 2]Closure Inspection
CPython implementation detail: a closure's captured free variables are stored as cell objects, accessible (mostly for debugging/introspection) via the function's __closure__ attribute, with the names available via __code__.co_freevars. This is rarely used in everyday code but is useful for understanding what's actually happening under the hood.
co_freevars lists the free-variable names; __closure__ is a tuple of cell objects (one per free variable) whose cell_contents holds the actual captured value.
def make_multiplier(factor: int):
def multiply(x: int) -> int:
return x * factor
return multiply
triple = make_multiplier(3)
print(triple.__code__.co_freevars) # ('factor',)
print([cell.cell_contents for cell in triple.__closure__]) # [3]The Classic Late-Binding Pitfall
Because a closure captures the VARIABLE (the cell), not the value at definition time, closures created in a loop that all reference the loop variable will all see its FINAL value when they're eventually called, not the value at the time each closure was created. This surprises almost everyone at least once.
Default arguments are evaluated once, at function-definition time, so 'i=i' copies the current value of i into a new parameter that each lambda owns independently.
# Surprising:
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] — all closures share the same 'i'
# Fixed with a default-argument snapshot:
funcs_fixed = [lambda i=i: i for i in range(3)]
print([f() for f in funcs_fixed]) # [0, 1, 2]Practical Uses of Closures
Closures show up constantly in real Python code: decorators wrap a function and remember the original function via a closure; function factories configure a family of related functions; callback-based APIs (event handlers, sorting keys) often rely on a closure to carry context. Related concept: decorators (covered separately) are one of the most common practical applications of closures.
make_validator produces a specialized, reusable predicate function; min_value and max_value are baked into it via closure rather than passed on every call.
def make_validator(min_value: int, max_value: int):
def validate(x: int) -> bool:
return min_value <= x <= max_value
return validate
is_valid_age = make_validator(0, 120)
print(is_valid_age(35)) # True
print(is_valid_age(200)) # FalseClosures vs Classes for Carrying State
A closure and a small class with instance attributes can solve the same problem — both carry state between calls. Reach for a closure when you need one function with a bit of hidden configuration/state; reach for a class when you need multiple related methods sharing state, inheritance, or a richer public interface.
Both give independent, stateful counters; the class version is more discoverable and extensible, the closure version is more compact for a single-function use case.
# Closure version
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
# Class version (equivalent capability, more structure)
class Counter:
def __init__(self):
self.count = 0
def increment(self) -> int:
self.count += 1
return self.countKey Takeaways
A closure is a nested function plus the free variables it captured from its enclosing scope at definition time.
Free variables are looked up via LEGB scoping; the closure keeps them alive even after the enclosing function has returned.
Assigning to a free variable requires nonlocal; mutating a mutable free variable in place does not.
Each call to the outer function creates a fresh, independent closure with its own captured state.
Closures in loops capture the variable, not a per-iteration snapshot — use a default-argument idiom to freeze the value if needed.
CPython implementation detail: captured variables live in cell objects, inspectable via __closure__ and co_freevars.
def outer(): total = 0 def add(x): total = total + x # UnboundLocalError return total return add def outer(): total = 0 def add(x): nonlocal total total += x return total return add
Assigning to 'total' anywhere in add's body makes Python treat it as local for the whole function; nonlocal tells Python to rebind the enclosing variable instead.
callbacks = [lambda: i * i for i in range(5)] results = [cb() for cb in callbacks] # [16, 16, 16, 16, 16] callbacks = [lambda i=i: i * i for i in range(5)] results = [cb() for cb in callbacks] # [0, 1, 4, 9, 16]
All the lambdas share the same free variable 'i'; by the time they're called, the loop has finished and i is 4. Binding i as a default argument captures its value at each iteration.
def make_logger(): def log(msg): history = [] # reset every call — not a closure over shared state history.append(msg) return history def make_logger(): history = [] def log(msg): history.append(msg) return list(history) return log
Declaring history inside the inner function makes it a fresh local on every call, defeating the purpose of a closure; it must live in the enclosing scope to persist across calls.
What makes a nested function a closure rather than just a nested function?
It references at least one free variable from its enclosing function's scope and is used (returned, stored, or passed) after that enclosing call would otherwise have ended, so it must retain access to that captured environment.
Why does assigning to a variable inside a nested function normally create a new local instead of modifying the enclosing one?
Python determines a name's scope statically by scanning the function body at compile time; any assignment to a name anywhere in the function makes that name local for the ENTIRE function unless declared nonlocal or global, which is why an assignment before nonlocal raises UnboundLocalError on read.
Why can a closure mutate a list it captured without using nonlocal, but not reassign it?
Mutating (e.g. append) modifies the existing object the name refers to and involves no assignment to the name; reassigning (e.g. values = []) would bind the name to a new object, which requires nonlocal to affect the enclosing scope.
Two calls to the same factory function both return closures over a variable with the same name. Do they share state?
No — each call to the factory function creates a brand new enclosing scope with its own instance of that variable, so the two closures capture two independent variables.
What does __closure__ return for a function with no free variables?
None, since there is nothing captured; only functions that actually reference names from an enclosing function scope get a populated __closure__ tuple.
Why do all lambdas built in a for-loop like [lambda: i for i in range(3)] return the same value when called later?
They all close over the same variable i, not its per-iteration value; by the time any lambda is called, the loop has finished and i holds its final value, which all the closures see.
When would you prefer a closure over a small class for carrying state between calls?
When you need exactly one function's worth of behavior with a bit of hidden configuration or accumulated state, and don't need multiple methods, inheritance, or a discoverable public interface — a class fits better once those needs appear.
Write a function make_power(exponent) that returns a closure computing base ** exponent for any base passed to it later.
def make_power(exponent: int): def power(base: float) -> float: return base ** exponent return power square = make_power(2) cube = make_power(3) print(square(5)) # 25 print(cube(2)) # 8 make_power captures 'exponent' as a free variable; each call to make_power produces an independent closure with its own exponent baked in.
What does this print?
True, False, True — 'state' starts False and each call to toggle() flips and returns it, and nonlocal ensures the flip persists between calls on the same closure.
Write a closure-based memoize(fn) decorator-like factory (without using functools) that caches results of a single-argument function.
def memoize(fn): cache = {} def wrapper(arg): if arg not in cache: cache[arg] = fn(arg) return cache[arg] return wrapper calls = 0 def slow_square(x): global calls calls += 1 return x * x fast_square = memoize(slow_square) print(fast_square(4), fast_square(4), calls) # 16 16 1
The 'cache' dict is a free variable shared by every call to wrapper; the second call to fast_square(4) finds 4 already cached and never calls slow_square again, so calls stays 1.
What happens when this runs?
UnboundLocalError. The assignment 'x = 20' anywhere in inner's body makes x local to inner for the whole function, so 'print(x)' tries to read the local x before it has been assigned — nonlocal x is required to reach outer's x.
def make_toggle():
state = False
def toggle():
nonlocal state
state = not state
return state
return toggle
t = make_toggle()
print(t())
print(t())
print(t())def outer():
x = 10
def inner():
print(x)
x = 20
inner()
outer()