Higher-Order Functions

Difficulty: Intermediate

In Python, functions are first-class objects. This means they can be assigned to variables, stored in data structures like lists and dictionaries, passed as arguments to other functions, and returned as values from functions. This property is the foundation of functional programming in Python and enables powerful patterns like callbacks, strategy dispatch, and function composition.

A higher-order function is any function that takes one or more functions as arguments, returns a function, or both. You have already seen built-in higher-order functions like `map()`, `filter()`, and `sorted()`. Writing your own higher-order functions lets you create abstractions that separate what to do from how to do it. For example, a `retry` function can take any operation as a parameter and handle the retry logic generically.

Passing functions as arguments is one of the most common patterns. When you pass a function to another function, the receiving function can call it whenever needed, with whatever arguments it chooses. This is the callback pattern. In Python, you pass the function object itself (without parentheses); adding parentheses would call it immediately and pass the return value instead.

Returning functions from functions creates what are known as function factories or configurators. The outer function sets up some configuration or state, and the inner function uses that state when called later. This pattern works because of closures -- the returned function retains access to the variables from the enclosing scope. Decorators, one of Python's most distinctive features, are simply higher-order functions that take a function and return a modified version of it.

Function composition is another powerful technique enabled by higher-order functions. You can create a new function that applies multiple transformations in sequence by chaining functions together. While Python does not have a built-in composition operator, you can easily build a `compose` utility that takes multiple functions and returns a single function that applies them in order.

Code examples

Functions as First-Class Objects

def shout(text):
    return text.upper()

def whisper(text):
    return text.lower()

# Assign function to a variable
transform = shout
print(transform("hello"))

transform = whisper
print(transform("HELLO"))

# Store functions in a list
operations = [shout, whisper, len]
for op in operations:
    print(f"{op.__name__}: {op('Python')}")

Functions can be assigned to variables, stored in lists, and iterated over. The `__name__` attribute reveals the original function name. Notice that `len` is a built-in function but works exactly the same way as user-defined ones -- all functions are first-class.

Passing Functions as Arguments

def apply_operation(func, values):
    """Apply a function to each value and return results."""
    return [func(v) for v in values]

numbers = [1, -2, 3, -4, 5]

print(apply_operation(abs, numbers))
print(apply_operation(lambda x: x  2, numbers))
print(apply_operation(str, numbers))

The `apply_operation` function does not know or care what transformation is applied -- it delegates that to the function passed in. This separation of concerns is the core benefit of higher-order functions.

Returning Functions (Function Factory)

def make_validator(min_len, max_len):
    """Return a function that validates string length."""
    def validate(text):
        length = len(text)
        if length < min_len:
            return f"Too short ({length} < {min_len})"
        if length > max_len:
            return f"Too long ({length} > {max_len})"
        return "Valid"
    return validate

username_ok = make_validator(3, 20)
password_ok = make_validator(8, 50)

print(username_ok("Al"))
print(username_ok("Alice"))
print(password_ok("short"))
print(password_ok("a_very_secure_password"))

make_validator is a function factory that produces configured validator functions. Each returned function closes over the min_len and max_len values that were passed when it was created.

Function Composition

def compose(*funcs):
    """Compose multiple functions: compose(f, g, h)(x) = f(g(h(x)))."""
    from functools import reduce
    def composed(x):
        return reduce(lambda acc, f: f(acc), reversed(funcs), x)
    return composed

double = lambda x: x * 2
add_ten = lambda x: x + 10
square = lambda x: x  2

pipeline = compose(square, add_ten, double)

# double(3) = 6 -> add_ten(6) = 16 -> square(16) = 256
print(pipeline(3))
print(pipeline(5))
print(pipeline(0))

The compose function chains functions right-to-left. For pipeline(3): double(3)=6, add_ten(6)=16, square(16)=256. This pattern lets you build complex transformations from simple, reusable building blocks.

Key points

Concepts covered

First-Class Functions, Passing Functions as Arguments, Returning Functions