Skip to content

Function Factories

A function factory is a function that creates and returns another function. This pattern leverages closures to generate specialized functions dynamically.

Mental Model

A function factory is a template with blanks: you call it with configuration values, and it hands back a new function with those values baked in via a closure. Each call produces an independent function that remembers its own captured state, like stamping out custom tools from the same mold.

Basic Pattern

A function factory returns an inner function that captures variables from the enclosing scope:

```python def make_multiplier(n: int): # returns a function: int -> int def multiply(x: int) -> int: return x * n return multiply

double = make_multiplier(2) triple = make_multiplier(3)

print(double(5)) # 10 print(triple(5)) # 15 ```

Each call to make_multiplier creates a new function with its own captured value of n.

How Closures Work

The inner function multiply remembers the value of n even after make_multiplier has finished executing. This combination of a function and the variables it captures from its enclosing scope is called a closure.

```python def make_adder(n: int): # returns a function: int -> int def add(x: int) -> int: return x + n return add

add_10 = make_adder(10) add_100 = make_adder(100)

print(add_10(5)) # 15 print(add_100(5)) # 105 ```

add_10 and add_100 are independent closures — each holds its own copy of n.

Practical Examples

Validators

```python def make_range_validator(min_val: float, max_val: float): # returns a function: float -> bool def validate(x: float) -> bool: return min_val <= x <= max_val return validate

valid_percentage = make_range_validator(0, 100) valid_age = make_range_validator(0, 150)

print(valid_percentage(50)) # True print(valid_percentage(150)) # False print(valid_age(25)) # True ```

Greeting Functions

```python def make_greeter(greeting: str): # returns a function: str -> str def greet(name: str) -> str: return f"{greeting}, {name}!" return greet

say_hello = make_greeter("Hello") say_hi = make_greeter("Hi")

print(say_hello("Alice")) # Hello, Alice! print(say_hi("Bob")) # Hi, Bob! ```

Formatters

```python def make_formatter(prefix: str, suffix: str = ""): # returns a function: value -> str def format_value(value) -> str: return f"{prefix}{value}{suffix}" return format_value

format_currency = make_formatter("$", " USD") format_percent = make_formatter("", "%")

print(format_currency(100)) # $100 USD print(format_percent(85.5)) # 85.5% ```

Power Functions

```python def make_power(exp: int): # returns a function: float -> float def power(base: float) -> float: return base ** exp return power

square = make_power(2) cube = make_power(3)

print(square(4)) # 16 print(cube(4)) # 64 ```

Modifying Captured Variables with nonlocal

A closure can read captured variables freely, but to modify them it needs the nonlocal keyword. Without nonlocal, an assignment inside the inner function would create a new local variable instead of updating the captured one.

```python def make_counter(start: int = 0, step: int = 1): # returns a function: () -> int count = start def counter() -> int: nonlocal count current = count count += step return current return counter

counter = make_counter() print(counter()) # 0 print(counter()) # 1 print(counter()) # 2

by_tens = make_counter(start=10, step=10) print(by_tens()) # 10 print(by_tens()) # 20 ```

Late Binding in Loops

Creating closures inside a loop is a common source of bugs. Because closures capture variables by reference (not by value), all the closures share the same variable — and see its final value after the loop ends.

```python

Bug: all functions capture the same variable i

funcs = [] for i in range(3): funcs.append(lambda x: x + i)

print(funcs0) # 12 (not 10!) print(funcs1) # 12 (not 11!)

Fix: capture the current value with a default parameter

funcs = [] for i in range(3): funcs.append(lambda x, i=i: x + i)

print(funcs0) # 10 print(funcs1) # 11 ```

Key Ideas

Function factories produce specialized functions from a common template. The inner function captures variables from the enclosing scope, forming a closure that remembers those values across calls. This is useful whenever you need a family of related functions that differ only in their configuration — validators, formatters, converters, and similar patterns.

When the inner function needs to modify a captured variable (not just read it), use nonlocal. When your factory grows complex enough to need multiple methods or shared mutable state, consider using a class instead.

For more on the decorator pattern — a close relative of function factories — see the decorators chapter.


Exercises

Exercise 1. Write a function factory make_validator(min_val, max_val) that returns a function accepting a single number and returning True if it is within the range [min_val, max_val], or False otherwise. Create validators for percentages (0--100) and temperatures (-40--50) and test them.

Solution to Exercise 1
def make_validator(min_val, max_val):
    def validate(value):
        return min_val <= value <= max_val
    return validate

is_percentage = make_validator(0, 100)
is_temperature = make_validator(-40, 50)

print(is_percentage(50))     # True
print(is_percentage(150))    # False
print(is_temperature(-30))   # True
print(is_temperature(60))    # False

Exercise 2. Create a make_counter(start=0) factory that returns a function. Each call to the returned function increments the counter by 1 and returns the new value. Use nonlocal to update the captured variable. Demonstrate that two counters created from the factory maintain independent state.

Solution to Exercise 2
def make_counter(start=0):
    count = start
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

counter_a = make_counter()
counter_b = make_counter(10)

print(counter_a())  # 1
print(counter_a())  # 2
print(counter_b())  # 11
print(counter_b())  # 12
print(counter_a())  # 3  (independent from counter_b)

Exercise 3. Write a factory make_formatter(template) that accepts a format string with a single {} placeholder and returns a function that inserts its argument into the template. For example, make_formatter("Hello, {}!")("Alice") should return "Hello, Alice!". Then demonstrate the late-binding pitfall by creating formatters in a loop and show how to fix it with a default argument.

Solution to Exercise 3
def make_formatter(template):
    def formatter(value):
        return template.format(value)
    return formatter

hello = make_formatter("Hello, {}!")
print(hello("Alice"))  # Hello, Alice!

# Late-binding pitfall
formatters_buggy = []
for prefix in ["INFO", "WARN", "ERROR"]:
    formatters_buggy.append(lambda msg: f"[{prefix}] {msg}")

# All use "ERROR" because prefix is captured by reference
print(formatters_buggy[0]("test"))  # [ERROR] test  (bug!)

# Fix with default argument
formatters_fixed = []
for prefix in ["INFO", "WARN", "ERROR"]:
    formatters_fixed.append(lambda msg, p=prefix: f"[{p}] {msg}")

print(formatters_fixed[0]("test"))  # [INFO] test
print(formatters_fixed[1]("test"))  # [WARN] test
print(formatters_fixed[2]("test"))  # [ERROR] test