Functions as return values is a powerful Python feature that lets you create and return new functions dynamically from other functions — enabling factories, closures, decorators, customizable behavior, and code generation at runtime. By returning a function reference (without calling it), you can build specialized, reusable functions based on input parameters, state, or configuration. In 2026, this pattern remains essential — it powers decorators, higher-order functions, strategy factories, ML model wrappers, configurable data transformers in pandas/Polars, and plugin-like systems where behavior is created on demand without if-elif chains or global state.
Here’s a complete, practical guide to returning functions in Python: basic factory pattern, closures for state, parameterized behavior, real-world patterns, and modern best practices with type hints, functools.partial, decorators, and pandas/Polars integration.
Basic factory — outer function takes parameters and returns an inner function customized with those values.
def create_adder(increment: int):
def adder(n: int) -> int:
return n + increment
return adder
add_five = create_adder(5)
add_ten = create_adder(10)
print(add_five(3)) # 8
print(add_ten(3)) # 13
Closures — returned function remembers outer scope variables (even after outer returns).
def make_counter(start: int = 0):
count = start
def counter() -> int:
nonlocal count
count += 1
return count
return counter
c1 = make_counter() # starts at 0
c2 = make_counter(100) # starts at 100
print(c1()) # 1
print(c1()) # 2
print(c2()) # 101
print(c2()) # 102
Real-world pattern: dynamic transformers in pandas/Polars — return customized functions based on config or column type for reusable pipelines.
import pandas as pd
def make_normalizer(mean: float = None, std: float = None):
def normalizer(col):
if mean is None or std is None:
return (col - col.mean()) / col.std()
return (col - mean) / std
return normalizer
df = pd.DataFrame({'sales': [100, 200, 300], 'visits': [10, 20, 30]})
# Create normalizer with precomputed stats
sales_norm = make_normalizer(df['sales'].mean(), df['sales'].std())
df['sales_norm'] = sales_norm(df['sales'])
# Or use default (compute on the fly)
df['visits_norm'] = make_normalizer()(df['visits'])
print(df)
Best practices make returned functions safe, readable, and performant. Use type hints — def create_adder(n: int) -> Callable[[int], int] — signals return type clearly. Prefer closures for stateful factories — nonlocal to modify outer variables. Modern tip: use Polars expressions or pandas method chaining — return lambda or partial for composable transforms. Use functools.partial — pre-bind arguments for reusable partial functions. Use decorators — wrap returned functions for logging/timing/validation. Return pure functions when possible — easier to test/compose. Avoid deep nesting — extract to top-level if complex. Test returned functions independently — call them in isolation. Combine with abc.ABC or typing.Protocol — define callable interfaces for type safety. Use __call__ in classes — make objects callable like functions for advanced factories. Use yield inside returned generators — for lazy sequences.
Returning functions from functions creates factories and closures — customize behavior dynamically with parameters or state. In 2026, use type hints, closures for state, partials/decorators for reuse, Polars/pandas for composable pipelines. Master returning functions, and you’ll write flexible, configurable, testable Python code that generates exactly the behavior you need on demand.
Next time you need a specialized function — return one from another. It’s Python’s cleanest way to say: “Give me a function tailored to this input.”