096. Nested Functions And Closures
Create functions that remember their surrounding context
096. Nested Functions And Closures
Rohan needs several different alert thresholds — 80% for a warning, 90% for critical, 95% for emergency. He could write three separate functions, or he could write one factory.
Without closures:
def is_warning(percent): return percent >= 80
def is_critical(percent): return percent >= 90
def is_emergency(percent): return percent >= 95This works but is repetitive. With a factory:
def make_threshold_checker(threshold):
def check(percent):
return percent >= threshold # 'threshold' captured from outer scope
return check
is_warning = make_threshold_checker(80)
is_critical = make_threshold_checker(90)
is_emergency = make_threshold_checker(95)Each returned check function is a closure — it remembers the threshold value that was in scope when make_threshold_checker was called. The three closures are independent; each has its own captured threshold.
Rohan also builds a formatter factory:
def make_formatter(label, unit="MB"):
def format_value(value):
return f"[{label}] {value:.1f} {unit}"
return format_value
fmt_ram = make_formatter("RAM", "MB")
fmt_swap = make_formatter("SWAP", "MB")Why closures instead of classes? For simple “function + config” patterns, a closure is lighter than writing a class with
__init__and__call__. When the state gets complex, switch to a class.
💡 Fun fact: Closures are the foundation of Python decorators — @functools.lru_cache, @property, @staticmethod are all closures under the hood. The entire decorator pattern is built on the idea that a function can return another function that “wraps” the original. This was popularized by Python’s decorator syntax introduced in PEP 318 (Python 2.4, 2004).
⚠️ Watch out: The classic closure-in-a-loop bug: [lambda: i for i in range(3)] creates three lambdas that all return the same i — the final value 2. Each lambda captures a reference to i, not a copy of it. The fix is lambda i=i: i — using a default argument to snapshot the value at the time the lambda is created.
🤔 Think about it: make_threshold_checker(80) and make_threshold_checker(90) return two independent closures, each remembering a different threshold. How does Python store these captured values — does each closure get its own copy of threshold, or do they share something? What does this tell you about how Python manages memory for closures?
Learning objectives
- Create inner functions that close over outer variables
- Return functions from functions (function factories)
- Understand that each closure has its own independent captured state
- Distinguish returning a function from returning its result
Key concepts
- closure
- nested function
- function factory
- enclosing scope
Try it
Concept detail
A closure is an inner function that “remembers” variables from its enclosing scope even after the outer function returns.
How it works: def make_adder(n): def add(x): return x + n # n is “closed over” — captured from make_adder’s scope return add
add5 = make_adder(5) # n=5 is stored inside the closure add5(10) # → 15 (n is still 5, even though make_adder is done) add5(3) # → 8
Each call to make_adder creates an independent closure with its own n: add5 = make_adder(5) # n=5 add10 = make_adder(10) # n=10 — completely separate from add5’s n
Three bugs to watch for:
- Hardcoding instead of using the captured variable (x * 2 instead of x * n).
- Returning the data instead of the inner function (return result vs return add).
- Incomplete range checks (>= min only, forgetting <= max).
To modify a captured variable inside the inner function, use nonlocal: def make_counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc
Closures are the foundation for decorators, memoization, and callback patterns. When the enclosed state grows complex, a class with call is more readable.
Solution
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
def make_adder(n):
def add(x):
return x + n
return add
def make_validator(min_val, max_val):
def validate(value):
return min_val <= value <= max_val
return validateTests
def test_multiplier_by_3():
triple = make_multiplier(3)
assert triple(5) == 15
assert triple(10) == 30
def test_multiplier_by_1():
identity = make_multiplier(1)
assert identity(7) == 7
def test_adder():
add5 = make_adder(5)
assert add5(10) == 15
assert add5(0) == 5
def test_validator_in_range():
validate = make_validator(0, 100)
assert validate(50) == True
assert validate(0) == True
assert validate(100) == True
def test_validator_out_of_range():
validate = make_validator(0, 100)
assert validate(-1) == False
assert validate(101) == False