Closures and Captured Variables
20 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A function can be built inside another function and keep using the outer function's variables after it has finished. This idea, the closure, is the base of decorators.
Functions are values
First-class function means a function is an ordinary value: you can assign it to a name, store it in a collection, pass it as an argument and return it from another function.
In Python a function is an object. You can store it in a variable or a dictionary, pass it to another function, and return it from one.
def double(x): return x * 2
def apply(func, value): return func(value)
ops = {"double": double, "negate": lambda x: -x}print(apply(double, 5))print(ops["negate"](4))Example 1 · Functions as values
def apply_tax(price): return round(price * 1.18, 2)
def apply_discount(price): return price * 0.9
steps = [apply_discount, apply_tax]price = 1000for step in steps: price = step(price)print(price)1062.0
What this code does
- A list can hold functions, and a loop can call them in turn.
- The result is 1000 reduced by 10 percent, then raised by 18 percent.
apply does not care which function it gets. It just calls it.
A closure remembers its surroundings
Closure is an inner function together with the variables it uses from the enclosing function. The variables stay available after the outer function has returned.
When an inner function uses a variable of the outer function, Python keeps that variable alive for it. The inner function plus those kept variables is a closure.
def make_discount(percent): def apply(price): return price * (100 - percent) / 100 return apply
ten_off = make_discount(10)print(ten_off(200))print(ten_off.__closure__[0].cell_contents)Example 2 · Two closures, two memories
def make_discount(percent): def apply(price): return price * (100 - percent) / 100 return apply
ten_off = make_discount(10)twenty_off = make_discount(20)print(ten_off(500), twenty_off(500))450.0 400.0
What this code does
- Each call to
make_discountcreates a newapplywith its ownpercent. - The two functions do not share anything.
make_discount has finished, yet ten_off still knows percent. Each call to make_discount makes a separate closure with its own value.
nonlocal lets an inner function change state
nonlocal is a statement that tells Python an assigned name belongs to the nearest enclosing function, not to the current one.
An inner function can read an outer variable freely. To assign to it, declare it nonlocal. Without that, the assignment makes a new local variable.
def make_counter(): count = 0
def next_id(): nonlocal count count += 1 return count
return next_id
ticket = make_counter()print(ticket(), ticket(), ticket())Example 3 · A counter with nonlocal
def make_counter(): count = 0
def next_id(): nonlocal count count += 1 return count
return next_id
orders = make_counter()invoices = make_counter()print(orders(), orders(), invoices())1 2 1
What this code does
ordersandinvoiceseach keep their owncount.- Remove the
nonlocalline and the call raisesUnboundLocalError.
Example 4 · A closure that does the job of a class
def make_account(balance): def deposit(amount): nonlocal balance balance += amount return balance return deposit
deposit = make_account(100)print(deposit(50))print(deposit(25))150 175
What this code does
- The balance lives in the closure, hidden from outside code.
- A class with
self.balancedoes the same job and is easier to grow with more methods.
Each call to make_counter() starts its own count, so two counters never disturb each other.
The late-binding trap in loops
Late binding means a closure looks up a variable's value when the function is called, not when it was created.
A closure captures the variable, not its value at that moment. Functions made in a loop all see the final value of the loop variable. A default argument freezes the current value.
broken = [lambda: i for i in range(3)]fixed = [lambda i=i: i for i in range(3)]print([f() for f in broken])print([f() for f in fixed])Example 5 · Fixing the late-binding trap
multipliers = [lambda x, n=n: x * n for n in (2, 3, 4)]print([f(10) for f in multipliers])
broken = [lambda x: x * n for n in (2, 3, 4)]print([f(10) for f in broken])[20, 30, 40] [40, 40, 40]
What this code does
- The default
n=nstores each loop value inside its own function. - The broken version multiplies every time by 4, the last loop value.
All three broken functions read the same i, which is 2 when they are finally called. In fixed, each function has its own default.
Next step
Check your understanding
12 quick questions