Type something to search...
Closures in Python: How Inner Functions Remember Their Environment

Closures in Python: How Inner Functions Remember Their Environment

Here's something that looks like it shouldn't work. A function creates a local variable, defines an inner function that uses it, and returns the inner function. The outer function has finished; its local variables should be gone. Yet when you call the inner function later, it still sees the variable, with the right value.

That's a closure: a function bundled with the variables it needs from the scope where it was defined. Closures are the foundation of decorators, callback factories, and many small "configure once, call many times" helpers. They're also the source of one of Python's most common surprises, the loop where every callback returns the same value.

This post explains what a closure is, how Python stores the captured variables, how nonlocal lets you update them, the late-binding trap and its fixes, practical uses, and how to decide between a closure and a small class.

A First Closure

def make_multiplier(factor: int):
    def multiply(x: int) -> int:
        return x * factor
    return multiply


double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5))  # 10 15

make_multiplier runs twice, and each run creates a fresh factor and a fresh multiply function. Each returned function remembers the factor from its own call. double and triple are built from the same code but carry different data.

Three conditions make this a closure:

  1. There's a nested function (multiply inside make_multiplier).
  2. The nested function refers to a variable from the enclosing function (factor).
  3. The nested function is used after the enclosing function has returned.

Where Python looks up names (local, enclosing, global, built-in) is covered in Python Scope Explained: LEGB, global, and nonlocal. This post focuses on the "E" in LEGB: what happens to enclosing variables once the enclosing function is gone.

How Python Remembers: Cells

Python doesn't keep the whole outer stack frame alive. When it compiles make_multiplier, it notices that factor is used by an inner function and stores it in a cell, a tiny container object, instead of a normal local slot. The inner function gets a reference to that cell.

You can inspect this:

print(double.__code__.co_freevars)
print(double.__closure__)
print(double.__closure__[0].cell_contents, triple.__closure__[0].cell_contents)
print(make_multiplier.__code__.co_cellvars)
('factor',)
(<cell at 0x100469ba0: int object at 0x100f4a0e0>,)
2 3
('factor',)
  • co_freevars lists the names the inner function uses from an enclosing scope (its free variables).
  • __closure__ is a tuple of cells, one per free variable. Each function object has its own tuple, which is why double and triple see different values.
  • co_cellvars on the outer function lists its locals that inner functions capture.

A function that doesn't capture anything has __closure__ set to None. That includes functions that only use globals: globals are looked up in the module each time, not captured.

Closures Capture Variables, Not Values

This is the single most important fact about closures. The cell holds the variable, and the inner function reads whatever the variable holds at the moment it runs:

def outer():
    x = 1

    def inner():
        return x

    x = 2
    return inner


print(outer()())  # 2, not 1

inner was defined while x was 1, but it returns 2 because x was reassigned before inner ran. Keep this in mind; it's the root of the loop trap later in this post.

Modifying Captured Variables with nonlocal

Reading a captured variable is automatic. Assigning to one is not. Here's a counter that tries:

def broken_counter():
    count = 0

    def increment():
        count += 1
        return count

    return increment


broken_counter()()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

Any assignment to a name inside a function makes that name local to the function, for the whole function body. So count += 1 refers to a local count, which hasn't been assigned yet. Python never looks at the enclosing scope.

Declare the name nonlocal to say "this refers to the variable in the enclosing function":

def make_counter():
    count = 0

    def increment() -> int:
        nonlocal count
        count += 1
        return count

    return increment


c1 = make_counter()
c2 = make_counter()
print(c1(), c1(), c1(), c2())  # 1 2 3 1

Each call to make_counter() creates a new cell, so c1 and c2 count independently. This is private, persistent state with no class and no global.

Mutating Is Not Assigning

You don't need nonlocal to mutate a captured object, only to rebind the name:

def make_history():
    items = []

    def add(item):
        items.append(item)  # mutation: no nonlocal needed
        return items

    return add


h = make_history()
h("a")
print(h("b"))  # ['a', 'b']

items.append() reads the variable items and calls a method on the list it points to. No assignment to items, so no nonlocal. Writing items = items + [item] would need it.

Several Functions Sharing One Cell

Inner functions created in the same call share the same cells, so they can cooperate on shared state:

def make_account(balance: float):
    def deposit(amount: float) -> float:
        nonlocal balance
        balance += amount
        return balance

    def get_balance() -> float:
        return balance

    return deposit, get_balance


deposit, get_balance = make_account(100)
deposit(50)
print(get_balance())                                       # 150
print(deposit.__closure__[0] is get_balance.__closure__[0])  # True

That's essentially an object with two methods and one private attribute, built from functions. It's a fun demonstration, but once you have several operations over shared state, a class is usually clearer (more on that below).

The Late-Binding Trap

Here's the classic bug. You create a list of callbacks in a loop and expect each one to remember its own i:

callbacks = [lambda: i for i in range(3)]
print([cb() for cb in callbacks])
[2, 2, 2]

Every lambda captured the same variable i, not its value at creation time. By the time you call them, the loop has finished and i is 2. The same thing happens with a for loop and def, with button handlers in a GUI, and with tasks scheduled in a loop. It's not a lambda problem; it's "closures capture variables" from earlier.

The fix is to give each function its own variable. There are three common ways.

1. A default argument. Defaults are evaluated when the function is defined, so each lambda stores the current value:

callbacks = [lambda i=i: i for i in range(3)]
print([cb() for cb in callbacks])  # [0, 1, 2]

It's short and idiomatic, but it adds a parameter that callers could accidentally override.

2. functools.partial. Bind the value explicitly:

from functools import partial


def show(i: int) -> int:
    return i


callbacks = [partial(show, i) for i in range(3)]
print([cb() for cb in callbacks])  # [0, 1, 2]

3. A factory function. Each call creates a new scope with a new cell:

def make_cb(i: int):
    return lambda: i


callbacks = [make_cb(i) for i in range(3)]
print([cb() for cb in callbacks])  # [0, 1, 2]

The factory is the most explicit version and the easiest to extend. partial is my default for simple cases because it says exactly what it does.

Practical Uses

Function Factories

Closures are a lightweight way to produce configured functions. You set the parameters once and hand out a simple callable:

def make_validator(min_len: int, required: str = ""):
    def validate(value: str) -> list[str]:
        errors = []
        if len(value) < min_len:
            errors.append(f"must be at least {min_len} characters")
        if required and required not in value:
            errors.append(f"must contain {required!r}")
        return errors

    return validate


validate_username = make_validator(3)
validate_email = make_validator(5, required="@")

print(validate_username("al"))
print(validate_email("ada@x.io"))
print(validate_email("adax"))
['must be at least 3 characters']
[]
['must be at least 5 characters', "must contain '@'"]

The code that uses validate_email just calls a function with one argument. It doesn't need to know about the configuration.

Encapsulated State

A closure can hide state that callers can't touch directly. Here's a simple sliding-window rate limiter:

import time
from collections.abc import Callable


def rate_limiter(max_calls: int, period: float) -> Callable[[], bool]:
    timestamps: list[float] = []

    def allow() -> bool:
        now = time.monotonic()
        while timestamps and now - timestamps[0] > period:
            timestamps.pop(0)
        if len(timestamps) < max_calls:
            timestamps.append(now)
            return True
        return False

    return allow


allow = rate_limiter(max_calls=2, period=1.0)
print([allow() for _ in range(4)])  # [True, True, False, False]

There's no way for calling code to clear timestamps by accident; the only interface is allow().

Decorators

Every decorator with a wrapper is a closure. The wrapper captures func (and, for decorators with arguments, the configuration too):

import functools


def make_tag(tag: str):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return f"<{tag}>{func(*args, **kwargs)}</{tag}>"
        return wrapper
    return decorator


@make_tag("h1")
def heading(text: str) -> str:
    return text


print(heading("Title"))  # <h1>Title</h1>

wrapper closes over both func and tag. If you understand closures, decorators are just a particular way of using them. Python Decorators Explained covers them in full.

Callbacks

Event handlers, sorted(key=...), map(), and threading targets all take a function. Closures let you pass in context without global variables:

def sort_by_distance(points: list[tuple[float, float]], origin: tuple[float, float]):
    ox, oy = origin

    def distance(p: tuple[float, float]) -> float:
        return ((p[0] - ox) ** 2 + (p[1] - oy) ** 2) ** 0.5

    return sorted(points, key=distance)


print(sort_by_distance([(5, 5), (1, 1), (3, 0)], origin=(0, 0)))
# [(1, 1), (3, 0), (5, 5)]

Closures vs Classes

Any closure can be rewritten as a class with __call__, and vice versa:

class Multiplier:
    def __init__(self, factor: int) -> None:
        self.factor = factor

    def __call__(self, x: int) -> int:
        return x * self.factor


m = Multiplier(2)
print(m(5))  # 10

How to choose:

Prefer a closure when...Prefer a class when...
There's one operationThere are several operations on the same state
The state is small and privateCallers need to read or inspect the state
You're writing a decorator or a callbackYou need __repr__, equality, pickling, or inheritance
A few lines would replace a class definitionDebugging would benefit from visible attributes

Closures are hard to inspect (you have to dig through __closure__), can't be pickled for multiprocessing, and can't easily be extended. Classes are more verbose but more discoverable. A good rule: once you're returning a tuple of functions, write a class.

Gotchas

Class Bodies Don't Create Closures

Methods and comprehensions inside a class body can't see class-level names as enclosing variables:

class Config:
    prefix = "app"
    keys = [prefix + "_" + k for k in ("a", "b")]
# NameError: name 'prefix' is not defined

The class body is a namespace, not an enclosing function scope, so the comprehension's iteration can't close over prefix. Inside methods, use self.prefix or type(self).prefix. In this example, compute the list outside the class or use a loop at class level.

Captured Objects Stay Alive

A closure keeps its captured objects alive for as long as the function exists:

def make_big():
    data = bytearray(10_000_000)

    def size() -> int:
        return len(data)

    return size


s = make_big()  # 10 MB stays in memory as long as `s` exists

If an inner function only needs a small piece of a large object, extract that piece into its own variable before defining the inner function, so the large object can be freed.

nonlocal Needs an Enclosing Function Variable

nonlocal x only works if x is a variable in an enclosing function. It can't refer to a module-level global (that's what global is for), and it's a SyntaxError if no enclosing function defines the name.

Conclusion

A closure is a function plus the cells holding the variables it uses from enclosing functions. Python creates those cells at compile time, gives each inner function its own __closure__ tuple, and keeps the cells alive as long as the function exists. That's how double remembers factor, how a counter keeps counting, and how every decorator's wrapper remembers the function it wraps.

Two rules cover most of the surprises: closures capture variables, not values, so loop callbacks need a default argument, partial, or a factory; and assigning to a captured name needs nonlocal. Use closures for small factories, callbacks, and decorators, and switch to a class when the state grows or callers need to see it.

Tags :
Share :

Related Posts

Abstract Base Classes in Python with the abc Module

Abstract Base Classes in Python with the abc Module

Python leans on duck typing: if an object has the method you need, you call it and move on. That works well until you have a family of classes that a

Continue Reading
*args and **kwargs in Python: Flexible Function Signatures

*args and **kwargs in Python: Flexible Function Signatures

You've seen def wrapper(*args, **kwargs): in decorators, and probably super().__init__(**kwargs) in class hierarchies. These two parameters let a

Continue Reading
Asyncio in Python: A Beginner's Guide to Asynchronous Programming

Asyncio in Python: A Beginner's Guide to Asynchronous Programming

A lot of programs spend most of their time waiting. A web scraper waits for pages to download, an API server waits for the database, a chat bot waits

Continue Reading