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.
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.