Skip to content

Python · Functions

Python Closures Explained — Free Variables, nonlocal, and the Loop Trap

3 min read Updated 2026-06-19 Share:

Practice Closures & Scope interview questions

Python closures, explained

A closure is a function that remembers variables from where it was defined, even after that enclosing scope has returned. Closures are the foundation of decorators, callbacks, and many functional patterns — and the late-binding loop trap is a perennial interview favourite.

What a closure is

A closure happens when a nested function references a variable from its enclosing function and is then returned (or otherwise outlives that function). The variable it "closes over" is called a free variable:

def make_multiplier(factor):
    def multiply(n):
        return n * factor     # factor is a free variable
    return multiply

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

Even though make_multiplier has returned, double still remembers factor == 2. Each call to make_multiplier creates an independent closure with its own captured state.

Inspecting the captured variables

The captured values live on the function object's __closure__, and the names on __code__.co_freevars:

double.__code__.co_freevars        # ('factor',)
double.__closure__[0].cell_contents # 2

You rarely need this, but it proves the value is genuinely stored with the function, not recomputed.

Closures capture variables, not values

This is the crucial subtlety: a closure stores a reference to the variable, and reads its current value when called, not when defined. With a single value that's invisible; in a loop it causes the classic bug:

funcs = []
for i in range(3):
    funcs.append(lambda: i)

[f() for f in funcs]    # [2, 2, 2] — all share the same i, now 2

All three lambdas close over the same i, which is 2 after the loop. The fix is to bind the current value with a default argument (evaluated at definition time):

funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]    # [0, 1, 2]

Using functools.partial(lambda i: i, i) works too — the point is to snapshot the value each iteration.

nonlocal — writing to a captured variable

By default a closure can read a free variable but not rebind it (assignment would make it local). nonlocal lets the inner function modify the enclosing variable, which is how you keep mutable state in a closure:

def make_counter():
    count = 0
    def increment():
        nonlocal count       # rebind the enclosing count
        count += 1
        return count
    return increment

c = make_counter()
c(), c(), c()    # (1, 2, 3)

Without nonlocal, count += 1 raises UnboundLocalError because the assignment makes count local to increment.

Closures vs classes

A closure with state is essentially a tiny object with one method. When do you pick which?

# Closure — concise when there's a single behaviour and a little state
def make_adder(n):
    return lambda x: x + n

# Class — clearer when there are multiple methods or richer state
class Adder:
    def __init__(self, n):
        self.n = n
    def __call__(self, x):
        return x + self.n
    def reset(self):
        ...

Use a closure for a single, simple captured behaviour (a callback, a configured function). Use a class when you need multiple related methods, introspectable state, or the object will grow.

Recap

A closure is a nested function that remembers free variables from its enclosing scope, giving each instance independent captured state — the basis of decorators and callbacks. Closures capture variables, not values, so functions built in a loop all see the final value unless you snapshot it with a default argument. Use nonlocal to rebind a captured variable (e.g. a running counter). Choose a closure for one simple behaviour and a class when state and methods grow.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel