A function defined inside another function can read variables from the outer scope. What is more surprising is that it can remember them after the outer function has finished and returned. That remembered bundle — a function plus the variables it captured — is called a closure.
Closures give you a way to attach private state to a function without building a class. They are the mechanism that makes factory functions and decorators work, and they appear in almost every Python codebase once you know what to look for. Understanding closures is the bridge between simple functions and the advanced patterns you will use in real libraries.
flowchart TD A["make_counter() runs"] --> B["count = 0"] B --> C["returns increment()"] C --> D["increment remembers count"] D --> E["each call: count += 1"] style C fill:#3776ab,color:#fff
How a closure works
When Python compiles a nested function, it notices which names come from outer scopes. Those names are stored in a cell object attached to the inner function. The cell is a reference, not a copy, so if the outer variable changes later, the inner function sees the new value.
In the example below, make_counter creates a local count, defines increment to read and update it, and returns increment. After make_counter finishes, its local scope would normally be thrown away — but because increment still holds a reference to count through its closure cell, the variable survives. Each factory call creates a fresh count in a fresh scope, so separate counters never interfere.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
c = make_counter()
print(c())
print(c())
print(c())
d = make_counter()
print('fresh:', d())
nonlocal versus global
Without nonlocal, assigning count inside increment would create a brand-new local variable shadowing the outer one. The outer count would stay at zero forever, and every call would return 1.
nonlocal says this name lives in an enclosing function scope, not the global module scope. It is the right tool for closures because it keeps the state private to the factory that created it. global would expose the variable at module level, where any code could touch it, defeating the purpose of encapsulation. Resist the temptation to use global as a quick fix; it makes code harder to test and reason about.
flowchart TD G["global x"] --> F["outer()"] F --> L["local x"] F --> I["inner() with nonlocal x"] I --> L style I fill:#3776ab,color:#fff
def make_prefixer(prefix):
def prefixer(text):
return prefix + ': ' + text
return prefixer
error = make_prefixer('ERROR')
info = make_prefixer('INFO')
print(error('disk full'))
print(info('started'))
print(error('network down'))
Closures versus classes
For very small pieces of state, a closure is often clearer than a class. A class needs __init__, self, and a method definition; a closure needs a nested function and a local variable. When the behaviour is just one method and the state is one or two variables, the closure wins on brevity and directness.
As the state grows richer — several variables, several methods, inheritance — a class becomes the better tool. The two are not competitors; they are points on a spectrum, and knowing when to switch is part of writing Pythonic code. A good rule of thumb is: if you find yourself writing more than one nested function or mutating more than two variables, consider graduating to a class.
flowchart LR A["for n in range(3)"] --> B["lambda: n"] B --> C["all see n=2"] D["lambda n=n: n"] --> E["each sees its own value"] style D fill:#3776ab,color:#fff
Reading closure state safely
Closures hide state by default, which is usually what you want. Occasionally you need to inspect that state for debugging or testing. You can expose it by returning the value from the inner function, or by attaching the value as an attribute on the function object itself. The latter is advanced and relies on the fact that function objects are mutable; for now, prefer designing your closure so the information you need is part of its return value or accessible through a separate accessor closure.
Write make_counter() that returns a function with no arguments. Each time the returned function is called, it increments a private count and returns the new value. The first call should return 1.
def make_counter():
# your code here
pass
Use a nested function that reads and updates a nonlocal count.
Return the nested function, not the count.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
def make_linear(scale, offset):
def linear(x):
return x * scale + offset
return linear
ten_plus = make_linear(10, 5)
print(ten_plus(2))
print(ten_plus(3))
What does this print? Pay attention to which variable each closure captures.
def make_adder(n):
def adder(x):
return x + n
return adder
a = make_adder(5)
b = make_adder(10)
print(a(1))
print(b(1))
make_adder(5) captures n=5; make_adder(10) captures n=10.
Each adder remembers its own n independently.
Write make_multiplier(n) that returns a function. The returned function takes one argument x and returns x * n. Do not use classes.
def make_multiplier(n):
# your code here
pass
The inner function captures n from the outer scope.
Return the inner function, not the result of calling it.
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
This code creates three multipliers in a loop, but they all return the same result. Fix the late-binding trap by capturing the current loop value in each closure.
def make_multipliers():
return [lambda x: x * i for i in range(1, 4)]
Add i=i as a default argument to freeze the current value.
Default arguments are evaluated at definition time, not call time.
def make_multipliers():
return [lambda x, i=i: x * i for i in range(1, 4)]
Write make_averager() that returns a function. The returned function accepts one number each call and returns the running average of every number it has ever received. Use a closure to track the total and count privately.
def make_averager():
# track total and count in the closure
pass
Use two nonlocal variables: total and count.
Return total / count after updating both.
def make_averager():
total = 0
count = 0
def averager(value):
nonlocal total, count
total += value
count += 1
return total / count
return averager
Recap
- A closure is a nested function plus the variables it captured from outer scopes.
- The inner function keeps those variables alive after the outer function returns.
nonlocallets the inner function assign to an enclosing scope variable without exposing it globally.- For simple state with one or two variables, closures are often clearer than classes.
- Loop-generated closures share the loop variable; freeze values with default arguments.
- Each factory call creates an independent scope, so separate closures never interfere.
Next you will use closures to build decorators — functions that wrap other functions to add behaviour transparently.