Day 24 · ~15m

Python Closures: Functions That Remember Their Environment

A function inside a function sees its parent's variables. That memory is a closure. A function with a backpack of captured variables, ready to use later.

student (curious)

I was looking at Amir's code last night. There's this function that returns a function. Actually returns it. And the function it returns uses variables from the outer function. How does that even work? Those variables should be gone.

teacher (focused)

Show me what you're looking at.

student (confused)

It's like this:

teacher (neutral)
def make_discount(pct):
    def apply(price):
        return price * (1 - pct / 100)
    return apply

ten_off = make_discount(10)
print(ten_off(100))  # prints 90

So pct is a parameter to make_discount. But when apply is running, make_discount is already done. Where does pct come from?

teacher (encouraging)

That is exactly the right question. Python lets the inner function see variables from the outer function's scope. The inner function captures those variables. It carries them with it, like a backpack, even after the outer function returns.

student (thinking)

So apply remembers what pct was when it was created?

teacher (neutral)

Not quite "remembers" — it has access to it. It is not a copy. It is a reference to the actual variable. Let me show you:

def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter())  # prints 1
print(counter())  # prints 2
print(counter())  # prints 3
student (surprised)

Wait, it actually modified the variable? Each time I call counter(), the count goes up?

teacher (serious)

Yes. And notice the nonlocal keyword. When you want to modify a captured variable inside the inner function, you have to say nonlocal first. Otherwise, Python thinks you are trying to create a new local variable.

student (confused)

What happens if I don't use nonlocal?

teacher (neutral)

Try it:

def make_counter():
    count = 0
    def increment():
        count += 1  # no nonlocal
        return count
    return increment

counter = make_counter()
print(counter())

You get an UnboundLocalError. Python sees count += and thinks you are trying to assign to count, which makes it a local variable. But you haven't given it a value yet, so it crashes.

student (thinking)

So nonlocal tells Python "I'm not making a new variable, I'm using the one from the parent scope."

teacher (encouraging)

Exactly. And here is the mental model: imagine the inner function has a backpack. When the function is created, it picks up all the variables from the outer scope and puts them in that backpack. When the function runs, later, it can look in the backpack to find those variables.

student (curious)

So it's not about the inner function remembering anything. It is carrying the variables with it. It has them.

teacher (proud)

That is a better way to think about it. The function is born with access to its parent's variables. That collection of variables plus the function is called a closure.

student (confused)

Is "closure" the function, or is it the function plus the variables?

teacher (serious)

Both. A closure is a function that can access variables from an enclosing scope. The function and its captured variables are bound together. You cannot separate them.

student (thinking)

Okay, back to Amir's discount code. pct is captured when apply is created. So each call to make_discount creates a new apply with a different pct?

teacher (focused)

Yes. Watch:

def make_discount(pct):
    def apply(price):
        return price * (1 - pct / 100)
    return apply

ten_off = make_discount(10)
twenty_off = make_discount(20)

print(ten_off(100))     # 90 — pct was 10
print(twenty_off(100))  # 80 — pct was 20

Two separate functions, two separate closures, two different captured values of pct.

student (excited)

So make_discount is a factory. It builds a function configured with a specific discount rate. I can call it once with 10%, save the result, and use that function forever to apply 10% discounts.

teacher (amused)

That is literally the entire pattern. Factories. Functions that build other functions with specific behavior baked in.

student (curious)

Is there anywhere you can see the captured variables? Like, can I print them out?

teacher (neutral)

There is a __closure__ attribute on functions. You can inspect it, but it is not something you do often. More interesting: watch what happens if you try this:

def outer():
    x = 10
    def inner():
        return x
    return inner

f1 = outer()
f2 = outer()
print(f1() == f2())  # Both return 10, but are they the same function?
print(f1 is f2)      # No — different function objects

Every call to outer() creates a new inner() function object, with its own closure.

student (focused)

So each time I call the factory, I get a new function, not a reused one.

teacher (encouraging)

And each one has its own captured variables. That is why the discount functions work independently — they are separate functions with separate closures.

student (thinking)

So a closure is when an inner function captures variables from the outer function, even after the outer function returns?

teacher (serious)

When an inner function can access variables from an enclosing scope. The outer function does not have to return the inner function — though that is the most common pattern. The inner function just has to be able to see the variables. But if you do not return the inner function, you cannot use it later, so returning it is how you create a closure you can actually call.

student (curious)

Tomorrow we learn decorators. Is that what the @ symbol is?

teacher (neutral)

A decorator is a closure that wraps another function. Same idea — a function that captures another function and uses it. But that is tomorrow. Today, understand that a function can be born inside another function, carry its parent's variables with it, and live independently after the parent returns. That is a closure.

student (excited)

So Amir's @require_auth decorator is probably a closure that wraps the route handler?

teacher (focused)

Almost certainly. And once you understand closures, you will understand decorators. Same backpack. Different thing being carried.

student (proud)

All right. Let me write a discount calculator. make_discount_calculator that takes a percentage and returns a function that discounts a price.

teacher (encouraging)

That is exactly what you need to do.