Previous | Tutorial index | Next
In Python, functions are first‑class citizens – they can be assigned to variables, passed as arguments, and returned from other functions. A closure is a powerful concept that arises when an inner (nested) function remembers and continues to have access to variables from its enclosing (outer) function, even after that outer function has finished executing. This allows functions to “carry along” some state without using global variables. Closures are the foundation for decorators and are also used in functional programming, callbacks, and factory functions. In this tutorial, you will learn how closures work, how to create them, and how to use nonlocal to modify captured variables.
A closure occurs when:
When you call the outer function, it returns the inner function, which “closes over” (captures) the variables it needs from the outer function’s scope. Those variables remain alive even after the outer function has returned.
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10)) # 20
print(triple(10)) # 30
factor is a local variable of make_multiplier.multiply is an inner function that uses factor.make_multiplier returns multiply, the factor variable is “captured” by the closure. Each returned function retains its own factor value (2 or 3).When Python compiles a nested function that references a variable from an outer scope, it creates a closure cell for that variable. The variable is stored in a special cell object that lives on the heap, not on the stack. This cell is shared between the outer and inner functions. Even after the outer function exits, the cell persists because the inner function holds a reference to it.
You can inspect a closure using the __closure__ attribute of the inner function:
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
print(add5.__closure__) # (<cell at 0x...: int object at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5
Each cell holds the captured value.
Python’s scope resolution follows the LEGB rule:
A closure is possible because the inner function can access variables in the enclosing scope (the “E” in LEGB). This access is resolved at runtime through the closure cells.
nonlocal Keyword – Modifying Captured VariablesBy default, if you try to assign to a variable from an enclosing scope, Python treats it as a new local variable in the inner function, thus breaking the closure. To modify a captured variable, you must declare it as nonlocal.
def counter():
count = 0
def increment():
nonlocal count # Without this, count would be local to increment
count += 1
return count
return increment
c = counter()
print(c()) # 1
print(c()) # 2
Without nonlocal, count += 1 would create a new local variable count inside increment (and raise UnboundLocalError when trying to read before assignment, because count is not defined locally). nonlocal tells Python to use the variable from the nearest enclosing scope that is not global.
nonlocal can also be used to modify variables in any enclosing (non‑global) scope, even multiple levels up.
As shown, you can create functions that are parameterized with some configuration:
def make_adder(n):
return lambda x: x + n # also a closure using lambda
add10 = make_adder(10)
print(add10(5)) # 15
Closures can encapsulate state without exposing it as global variables, similar to objects with private attributes:
def bank_account(initial_balance=0):
balance = initial_balance
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
if amount > balance:
raise ValueError("Insufficient funds")
balance -= amount
return balance
def get_balance():
return balance
return deposit, withdraw, get_balance
dep, wit, bal = bank_account(100)
print(dep(50)) # 150
print(wit(30)) # 120
print(bal()) # 120
Here, balance is not accessible from outside, providing data hiding.
Closures are often used to store extra data for callbacks:
def make_button_handler(message):
def handler():
print(f"Button clicked: {message}")
return handler
button_handlers = {
'save': make_button_handler("Save clicked"),
'delete': make_button_handler("Delete clicked"),
}
# Later, call handler['save']()
Closures can be used to build pipelines where each step remembers its parameters:
def compose(f, g):
def h(x):
return f(g(x))
return h
def add1(x): return x + 1
def square(x): return x * x
add1_then_square = compose(square, add1)
print(add1_then_square(3)) # 16 ( (3+1)^2 = 16 )
Useful when you need many functions that differ by a parameter:
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(5)) # 25
print(cube(5)) # 125
Closures and classes can both encapsulate state. Use closures when:
Use classes when you need many methods, inheritance, or more structure.
def create_multipliers():
multipliers = []
for i in range(3):
multipliers.append(lambda x: x * i)
return multipliers
for m in create_multipliers():
print(m(2)) # Output: 4, 4, 4 (all use i=2)
Fix: Bind the current value by creating a default argument:
def create_multipliers():
multipliers = []
for i in range(3):
multipliers.append(lambda x, i=i: x * i) # i=i captures the value
return multipliers
# Now: 0, 2, 4
Forgetting nonlocal: When you intend to modify a captured variable, always use nonlocal. Otherwise, Python creates a new local variable.
Memory leaks: Capturing large objects in a closure prevents them from being garbage collected as long as the closure exists. Be mindful of what you capture.
Use descriptive names: Closures can be harder to debug, so give meaningful names to inner functions and variables.
What is a closure?
Which keyword must you use to modify a variable from an enclosing scope?
globalnonlocalouterclosureGiven the following code, what is the output?
def outer():
x = 10
def inner():
return x
return inner
f = outer()
print(f())
10NoneError0True or False: A closure can capture variables from multiple enclosing scopes.
What is the purpose of __closure__ attribute?
What will be the output of this code?
def make_inc():
count = 0
def inc():
count += 1
return count
return inc
f = make_inc()
print(f())
10UnboundLocalErrorNoneHow can you fix the code in question 6?
global countnonlocal countcount = 0 inside inccount without incrementing.What is a common use of closures in GUI programming?
Given def make_adder(n): return lambda x: x + n, what does make_adder(5)(10) return?
51510NoneWhat is the late binding trap in closures?
Exercise 1: Simple Closure
Write a function make_counter(initial) that returns a closure that increments and returns the counter.
c = make_counter(5) print(c()) # 6 print(c()) # 7
</details>
**Exercise 2: Parameterized Greeter**
Write a function `greeter(greeting)` that returns a function that takes a name and returns a full greeting.
<details><summary>Sample Solution</summary>
```python
def greeter(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet
hello = greeter("Hello")
print(hello("Alice")) # Hello, Alice!
Exercise 3: Caching with Closures
Write a closure that caches results of slow_square(x) (use time.sleep(1) to simulate slowness).
sq = cached_square() print(sq(5)) # takes 1s print(sq(5)) # instant
</details>
**Exercise 4: Multi‑level Closure**
Write a function `outer(x)` that returns a function `middle(y)` that returns a function `inner(z)` that returns `x + y + z`.
<details><summary>Sample Solution</summary>
```python
def outer(x):
def middle(y):
def inner(z):
return x + y + z
return inner
return middle
result = outer(1)(2)(3)
print(result) # 6
Exercise 5: Closure with nonlocal
Write a function average_maker() that returns a closure that takes a number and returns the running average.
avg = average_maker() print(avg(10)) # 10.0 print(avg(20)) # 15.0 print(avg(30)) # 20.0
</details>
---
### 🏠 Homework – Deeper Thinking
#### Short Answer Questions
**1. Memoization with Closure**
Write a closure `memoize(func)` that caches results. Test with a slow Fibonacci function.
<details><summary>Sample Answer</summary>
```python
def memoize(func):
cache = {}
def wrapper(arg):
if arg not in cache:
cache[arg] = func(arg)
return cache[arg]
return wrapper
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
fib = memoize(fib)
print(fib(40)) # fast!
2. Function Registry
Create a closure that maintains a registry of functions (register and get).
register, get = registry_maker() register("add", lambda a,b: a+b) print(get("add")(3,4)) # 7
</details>
**3. Partial Application using Closures**
Implement `partial(func, *args)` that returns a closure that partially applies arguments.
<details><summary>Sample Answer</summary>
```python
def partial(func, *args):
def wrapper(*more_args, **kwargs):
return func(*(args + more_args), **kwargs)
return wrapper
def add(a, b, c):
return a + b + c
add5 = partial(add, 5)
add5_plus = add5(10)
print(add5_plus(3)) # 18
4. State Machine using Closures
Write a function make_state_machine(initial_state) that returns transition(event) and get_state() functions.
trans, get = make_state_machine('idle') trans('start') print(get()) # running trans('pause') print(get()) # paused
</details>
**5. Closure with Custom Iterator**
Write a closure that returns a function that yields the next value each call (like a counter). Compare to a generator.
<details><summary>Sample Answer</summary>
```python
def make_counter(initial=0):
count = initial
def next_value():
nonlocal count
result = count
count += 1
return result
return next_value
counter = make_counter(5)
print(counter()) # 5
print(counter()) # 6
# Generator version:
def gen_counter(initial=0):
count = initial
while True:
yield count
count += 1
g = gen_counter(5)
print(next(g)) # 5
print(next(g)) # 6
cache = {} in outer, inner function checks if arg in cache.def registry_maker():
reg = {}
def register(name, func):
reg[name] = func
def get(name):
return reg.get(name)
return register, get
partial should return lambda *args2, **kwargs2: func(*(args + args2), **kwargs2).count and a returned function that increments and returns count. Compare with yield.In this tutorial, you have learned:
nonlocal keyword and when to use it.Closures are a fundamental building block for advanced Python techniques, particularly decorators, which we will cover in the next tutorial (Part 2). Mastering closures will enable you to write more flexible, maintainable, and expressive code.
Next Steps: In Tutorial 10, we will dive into Decorators – functions that modify or enhance other functions using closures.
Happy closing!