Closures
A closure is a function that "remembers" the variables from the scope it was created in, even after that outer scope has already finished running. Closures are what make function factories and decorators possible in Python.
What Makes a Closure
A closure forms when three conditions are all true: there is a nested function, the inner function references a variable from the enclosing (outer) function, and the outer function returns the inner function.
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!" # 'greeting' comes from the enclosing scope
return greet
hello_greeter = make_greeter("Hello")
hi_greeter = make_greeter("Hi")
print(hello_greeter("Ada"))
print(hi_greeter("Ada"))Hello, Ada! Hi, Ada!
By the time hello_greeter("Ada") runs, make_greeter has already returned — its local scope is technically gone. Yet greet still has access to greeting. That value is "closed over" and kept alive inside the returned function, which is exactly what gives closures their name.
Worked Example: A Counter Factory
A classic closure example is a function that builds independent counters, each with their own private count that persists between calls.
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return increment
counter_a = make_counter()
counter_b = make_counter(start=100)
print(counter_a()) # 1
print(counter_a()) # 2
print(counter_a(step=5)) # 7
print(counter_b()) # 101
print(counter_b()) # 1021 2 7 101 102
counter_a and counter_b are two completely independent closures — each one has its own private count variable that no other code can reach directly.
`nonlocal` Is Required to Mutate Enclosed Variables
Reading an enclosing variable inside a nested function works automatically (as seen with greeting above). But reassigning it requires the nonlocal keyword — otherwise Python assumes you're creating a brand-new local variable.
def make_counter_broken():
count = 0
def increment():
count += 1 # UnboundLocalError! Python treats 'count' as local here
return count
return increment
broken = make_counter_broken()
broken() # raises UnboundLocalError: local variable 'count' referenced before assignmentUnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Inspecting a Closure
You can actually see what variables a closure has captured using the function's __closure__ attribute — useful for understanding what's going on under the hood.
def make_multiplier(factor):
def multiply(n):
return n * factor
return multiply
times_three = make_multiplier(3)
print(times_three(10))
print([cell.cell_contents for cell in times_three.__closure__])30 [3]
Practical Use Cases
Function factories: producing customized functions on demand, likemake_multiplier(3)ormake_greeter("Hi")above.Callbacks with state: giving a callback access to context without using global variables or a full class.Decorators: a decorator is itself a closure — it wraps a function and remembers it to call later. Covered in depth next.Data hiding: variables captured by a closure aren't directly accessible from outside, giving a lightweight form of encapsulation.
def make_cache():
store = {}
def cached_square(n):
if n not in store:
print(f"Computing square of {n}...")
store[n] = n * n
return store[n]
return cached_square
square = make_cache()
print(square(4)) # computes
print(square(4)) # cached, no "Computing" message
print(square(5)) # computesComputing square of 4... 16 16 Computing square of 5... 25