Course outline · 0% complete

0/27 lessons0%

Course overview →

Closures: functions that remember

lesson 5-1 · ~12 min · 13/27

One idea from lesson 2-3 is the seed of this whole unit. Writing say = shout, with no parentheses, makes say a second name for the shout function itself rather than calling it. Without parentheses you are handling the function as a value, so afterwards say and shout point at the same function object.

This unit pushes that idea further: functions defined inside other functions, and returned as values.

Functions can be built inside functions

Since functions are values, a function can create and return a new function:

def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

Here is the surprising part. make_multiplier has already finished by the time you call double(10), yet multiply still knows its factor. An inner function that keeps access to variables from the function that created it is called a closure. Each call to make_multiplier captures its own factor, so double and triple do not interfere.

Closures are not an academic curiosity. They are the machinery behind decorators (next lesson), behind callbacks that need to carry context with them, and behind every "configure once, call many times" function factory in production code.

make_multiplier(factor)returns the inner functiondoublecaptured factor = 2triplecaptured factor = 3call with 2call with 3the factory has already returned, yet each captured value lives on
Each call to the factory returns a new inner function carrying its own captured factor, so double and triple never interfere.

Two multipliers with separate memories

make_multiplier is a factory. Each call produces a new multiply function that has captured its own factor, so the two results share nothing.

def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))
print(triple(10))
print(double(triple(5)))

Output

20
30
30

factor is a parameter of make_multiplier, which finished running long before double(10) was ever called. The value survives anyway, because the returned function keeps a live reference to it. That captured environment is what the word closure names.

The last line composes them: triple(5) is 15, and double(15) is 30.

Mutable state in a closure

A closure can also carry state that changes between calls. To reassign a captured variable you must declare it nonlocal, otherwise Python would treat the assignment as a brand-new local variable:

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

Each counter made by make_counter has private, independent state, like a tiny object without a class. Closures are the engine behind decorators in the next lesson, so make sure this example feels solid before moving on.

make_greeter

make_greeter(greeting) returns a function that takes a name and produces "greeting, name!". Two calls to the factory yield two independent greeters.

def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet

hello = make_greeter("Hello")
howdy = make_greeter("Howdy")
print(hello("Ada"))
print(howdy("Linus"))

Output

Hello, Ada!
Howdy, Linus!

greet(name) is defined inside make_greeter, and the factory returns greet with no parentheses, handing back the function rather than calling it. The inner f-string reaches two variables from two different places: name is its own parameter, and greeting comes from the enclosing scope through the closure.

Every closure gets its own state

Suppose c1 = make_counter() and c2 = make_counter(), then you call c1(), c1(), and c2() in that order. That last call returns 1.

Every call to make_counter creates a fresh count variable, captured by a fresh inner function. c1 has counted twice and sits at 2, while c2 has counted only once. The two counters cannot interfere with each other, and independent state per closure is exactly the property that makes factories useful.

Why nonlocal is required

Deleting the nonlocal line from make_counter while keeping count += 1 produces an UnboundLocalError the first time the counter is called.

The rule behind it is that assigning to a name anywhere in a function makes that name local to the entire function. Python decides this when it compiles the function, not while it runs. So count += 1 creates a local count, but += must read the old value first, and the local count has never been assigned. Python raises UnboundLocalError rather than silently reaching outward.

nonlocal count is how you say you mean the enclosing function's variable, not a new local one. Without it, reading a closed-over variable works fine but rebinding it does not.