PythonClosures

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.

Python
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.

Python
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())       # 102
1
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.

Python
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 assignment
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Warning
Without `nonlocal count`, the line `count += 1` makes Python treat `count` as a new local variable inside `increment` — but that local variable has no initial value, since `count += 1` needs to read `count` before it can write to it. The fix is always to declare `nonlocal count` (or `global count`, for module-level variables) before reassigning.
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.

Python
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, like make_multiplier(3) or make_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.

Tip
If you find yourself writing a closure just to hold a couple of pieces of related state and behavior, consider whether a small class would be clearer. Closures are great for simple cases; classes scale better once there's more than one or two moving parts.
Example
A closure-based cache that remembers previous results without any global state or a class.

Python
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))  # computes
Computing square of 4...
16
16
Computing square of 5...
25
Note
Every time you call `make_cache()`, you get a brand-new, independent `store` dictionary — closures don't share state between separate calls to the outer factory function.