Returning Functions from Functions – Closures & Factories in Data Science 2026

Data Science Tool Box Mar 18, 2026

Returning Functions from Functions – Closures & Factories in Data Science 2026

One of the most powerful features in Python is the ability for a function to return another function. This pattern, known as function factories or closures, is extremely useful in data science for creating customized transformers, filters, scorers, and reusable data processing pipelines.

TL;DR — Why Return Functions?

  • To create reusable, configurable functions
  • To implement the "factory" pattern
  • To build closures that remember specific settings
  • To create decorators and higher-order functions

1. Basic Function Factory Example

def create_price_filter(threshold: float):
    """Returns a function that checks if price > threshold."""
    
    def is_expensive(price: float) -> bool:
        return price > threshold          # 'threshold' is remembered (closure)
    
    return is_expensive


import numpy as np
import pandas as pd

df = pd.DataFrame({"amount": [250.0, 1800.0, 999.0]})

# Usage
expensive_filter = create_price_filter(1500)
cheap_filter = create_price_filter(500)

df["is_expensive"] = df["amount"].apply(expensive_filter)
df["is_cheap"] = df["amount"].apply(cheap_filter)

2. Real-World Data Science Example

def create_feature_engineer(scaling_method: str = "standard"):
    """Factory that returns a customized feature engineering function."""
    
    def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
        df = df.copy()
        
        if scaling_method == "standard":
            df["amount_scaled"] = (df["amount"] - df["amount"].mean()) / df["amount"].std()
        elif scaling_method == "minmax":
            df["amount_scaled"] = (df["amount"] - df["amount"].min()) / (df["amount"].max() - df["amount"].min())
        
        df["log_amount"] = df["amount"].apply(lambda x: 0 if x <= 0 else np.log1p(x))
        
        return df
    
    return engineer_features


# Create different feature engineering pipelines
standard_engineer = create_feature_engineer("standard")
minmax_engineer = create_feature_engineer("minmax")

# Use them
df_standard = standard_engineer(df)
df_minmax = minmax_engineer(df)

3. Best Practices in 2026

  • Use function factories when you need to create multiple similar functions with different configurations
  • Document what the returned function does and what it "remembers"
  • Keep the inner function focused and small
  • Use closures for creating reusable data transformers and filters
  • Be aware that closures capture variables from the enclosing scope

Closures and Factories

A closure is an inner function that remembers variables from the scope where it was created, even after the outer function has returned. A function that builds and returns such inner functions is called a factory.

Factories are useful for creating families of similar functions, such as scalers with different factors or validators with different limits. They are a light alternative to writing a small class.

Worked Example: A Threshold Factory

We create filter functions with different limits from one factory.

def make_filter(limit: float):
    def keep(values):
        return [v for v in values if v >= limit]
    return keep

keep_50 = make_filter(50)
keep_100 = make_filter(100)
data = [20, 55, 100, 140]
print(keep_50(data))
print(keep_100(data))
print(keep_50.__closure__[0].cell_contents)

funcs = [lambda x, i=i: x + i for i in range(3)]
print([f(10) for f in funcs])

Each returned function keeps its own limit, which you can see in __closure__. The last lines show the i=i default trick, which captures the current loop value instead of the final one.

Practical Tips

Closures are also the building block of decorators. A decorator is a function that takes another function, wraps it in an inner function that adds behaviour such as timing or logging, and returns the wrapper. Understanding closures therefore makes decorators much easier to read.

In data work, factories help when you need many similar transformations, for example one scaling function per unit system or one validation rule per column. Each produced function stays tiny, while the shared logic lives in one place.

Keep factory arguments simple and immutable where possible. If a factory captures a list and that list is later changed elsewhere, every produced function will see the change, which can be surprising. Give produced functions descriptive variable names, such as keep_50, so their configuration stays obvious at the call site.

Common Mistakes

  • Expecting closures in loops to capture each loop value automatically.
  • Building factories where a plain parameter would do.
  • Forgetting to return the inner function.
  • Changing captured variables without nonlocal.

Quick Summary

  • Closures remember enclosing variables.
  • Factories return configured functions.
  • Use i=i to capture loop values.
  • Keep factories small and focused.

Conclusion

Returning functions from functions (function factories and closures) is a sophisticated but very useful technique in data science. In 2026, this pattern is commonly used to create configurable data preprocessors, custom filters, feature engineers, and model wrappers. When used correctly, it leads to cleaner, more reusable, and more maintainable code.

Next steps:

  • Identify repetitive data processing logic in your code and refactor it into a function factory that returns customized functions

Related Tutorials

Share:
Last updated: March 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices