Nested Functions and Closures

A closure is an inner function that keeps hold of variables from the function that created it, long after that function has returned.

Nested functions

def outer():
    def inner():
        return "from inner"
    return inner()


print(outer())              # from inner
# print(inner())            # NameError - inner exists only inside outer

Defining a function inside another is ordinary Python. The inner function is created fresh every time the outer one runs, and it is invisible from outside.

Use a nested function when a helper is meaningful only to one function, and when it needs access to that function's local variables.

What makes it a closure

def multiplier(factor):
    def multiply(n):
        return n * factor       # factor comes from the ENCLOSING scope
    return multiply


triple = multiplier(3)
print(triple(7))                # 21

multiplier has already returned by the time triple(7) runs. Its local variable factor should be gone. It is not, because multiply referred to it, so Python kept it alive attached to the function object.

multiplier(3)  ──►  creates multiply
                    and a cell holding factor = 3
                              │
triple ───────────────────────┘

triple(7)  ──►  looks up n locally, factor in its cell  ──►  21

Three conditions define a closure:

  1. There is a nested function.
  2. The inner function refers to a variable from the enclosing function.
  3. The outer function returns the inner one.

Seeing the captured values

def multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply


triple = multiplier(3)

print(triple.__closure__)                        # a tuple of cells
print(triple.__closure__[0].cell_contents)       # 3
print(triple.__code__.co_freevars)               # ('factor',)

Each closure has its own copy

double = multiplier(2)
triple = multiplier(3)

print(double(10), triple(10))       # 20 30
print(double.__closure__[0].cell_contents)     # 2
print(triple.__closure__[0].cell_contents)     # 3

Every call to the factory produces an independent function with its own captured environment.

Closures that hold state

def make_counter():
    count = 0

    def increment():
        nonlocal count           # without this, count += 1 raises
        count += 1
        return count

    return increment


tick = make_counter()
print(tick(), tick(), tick())     # 1 2 3

other = make_counter()
print(other())                    # 1 - a separate counter

nonlocal is required because count += 1 is an assignment, which would otherwise make count local to increment. Reading alone never needs it.

Returning several closures over one state

def make_account(balance=0):
    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 current():
        return balance

    return deposit, withdraw, current


deposit, withdraw, current = make_account(100)
deposit(50)
withdraw(30)
print(current())                  # 120

Three functions share one hidden variable. Nothing outside can reach balance directly, which is genuine encapsulation - the same thing a class gives you, with less ceremony.

Closures compared with classes

def make_counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment


class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count
ClosureClass
Less code for one behaviourClearer for several behaviours
State is genuinely privateState is inspectable
Hard to serialise or debugEasy to print and test
One function, one jobExtensible by subclassing

One captured value and one behaviour: use a closure. More than that: use a class.

The late binding trap

functions = []
for i in range(3):
    functions.append(lambda: i)

print([f() for f in functions])       # [2, 2, 2]

A closure captures the variable, not the value it held at creation time. All three functions share the loop variable i, and by the time they are called the loop has finished with i equal to 2.

# Fix 1: a default argument is evaluated at definition time
functions = [lambda i=i: i for i in range(3)]
print([f() for f in functions])       # [0, 1, 2]


# Fix 2: a factory gives each function its own scope
def make(value):
    return lambda: value


functions = [make(i) for i in range(3)]
print([f() for f in functions])       # [0, 1, 2]


# Fix 3: partial
from functools import partial
functions = [partial(lambda v: v, i) for i in range(3)]
print([f() for f in functions])       # [0, 1, 2]
This trap appears constantly with event handlers and callbacks created in a loop. If several callbacks all behave as though they belong to the last item, this is why.

Practical closures

A configurable validator

def min_length(n):
    def validate(text):
        return len(text) >= n
    return validate


checks = [min_length(3), min_length(8)]
print([check("hello") for check in checks])       # [True, False]

Memoising by hand

def memoise(func):
    cache = {}

    def wrapper(n):
        if n not in cache:
            cache[n] = func(n)
        return cache[n]

    return wrapper


def slow_square(n):
    print("computing", n)
    return n * n


fast = memoise(slow_square)
print(fast(4))      # computing 4 -> 16
print(fast(4))      # 16, no computation

That is a decorator without the @ syntax. Decorators are exactly this pattern with nicer notation, and they have their own note.

A rate limiter

def limit_calls(maximum):
    used = 0

    def call(func, *args):
        nonlocal used
        if used >= maximum:
            return "limit reached"
        used += 1
        return func(*args)

    return call


limited = limit_calls(2)
print(limited(str.upper, "a"))     # A
print(limited(str.upper, "b"))     # B
print(limited(str.upper, "c"))     # limit reached

Common mistakes

  • Forgetting nonlocal and meeting UnboundLocalError.
  • Using global where nonlocal was meant.
  • Falling into late binding when creating functions in a loop.
  • Returning inner() instead of inner.
  • Building a closure over a large object and keeping it alive far longer than intended.
  • Using a closure where a class would be easier to test and debug.

Best practices

  • Use a closure for a single configured behaviour; use a class beyond that.
  • Always bind loop values explicitly when creating functions in a loop.
  • Use nonlocal only for the variable you genuinely need to update.
  • Name factory functions so the call site reads well: min_length(3), not make(3).
  • Remember that a closure keeps its captured objects alive; do not capture more than you need.

Practice

  1. Write a factory producing a function that raises numbers to a fixed power.
  2. Build a counter that can also be reset, returning two closures over one variable.
  3. Demonstrate the late binding trap with three callbacks, then fix it in two ways.
  4. Write a memoising wrapper and prove it avoids recomputation.
  5. Rewrite the account example as a class and compare the two versions.

Conclusion

A closure is a function plus the variables it captured from where it was defined. It gives you private state without a class, it requires nonlocal to update that state, and it captures variables rather than values - which is the whole explanation of the late binding trap.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Trees and Graphs

A tree is a graph with no cycles and one root. Both are walked with the same two strategies - depth first with a stack, breadth first with a queue.

Read more
Python

Sorting Algorithms

Python sorts for you in n log n. Implementing bubble, insertion, merge and quick sort is still worth doing, because it teaches how algorithms are comp...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.