← Home

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 >= 95

This 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:

  1. Hardcoding instead of using the captured variable (x * 2 instead of x * n).
  2. Returning the data instead of the inner function (return result vs return add).
  3. 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 validate

Tests

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

Resources