Higher-Order Functions in Python

Learn what higher-order functions are, how to use Python's built-in map, filter, and sorted, and how to write your own functions that accept or return other functions.

7 min read

The article on first-class functions in Python established that functions are objects that can be assigned, stored, and passed as arguments. A higher-order function is simply a function that takes advantage of this property: it accepts another function as an argument, returns a function as its result, or both. This is not a separate language feature but a design pattern that emerges naturally from first-class functions.

Python's built-in functions include several higher-order functions that you have already encountered. The sorted function accepts a key argument that is itself a function. The map function accepts a transformation function and applies it to every element of an iterable. The filter function accepts a predicate function and keeps only the elements for which it returns True. Each of these functions separates the orchestration logic, iterating, collecting, ordering, from the specific operation, transforming, testing, extracting a key, which the caller supplies as a function argument. This separation is the core idea behind higher-order functions.

How higher-order functions work

Consider what sorted does when you pass it a key function. Sorted handles the entire sorting algorithm: comparisons, swaps, partitioning, all the mechanics of putting elements in order. But it does not know what "order" means for your specific data until you tell it. The key function bridges that gap. Sorted calls your key function on each element, receives a sort key back, and uses those keys to determine the element order:

pythonpython
words = ["python", "a", "function", "code"]
by_length = sorted(words, key=len)
print(by_length)  # ['a', 'code', 'python', 'function']

The len function is passed as an argument without being called. Sorted calls len on each word, collects the lengths, and sorts the words by those lengths. If you later need to sort by the last letter instead, you write a different key function and pass it to the same sorted. The sorting algorithm does not change; only the key extraction does. This is the value of higher-order functions: they isolate the part of the behavior that varies so the rest of the logic stays constant and reusable.

Map and filter follow the same pattern. Map iterates over an iterable, calls a function on each element, and collects the results. Filter iterates, calls a predicate on each element, and keeps elements where the predicate returns True. The iteration logic is identical regardless of the transformation or predicate, so map and filter accept those as function arguments:

pythonpython
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))

The lambda passed to map is the transformation. The lambda passed to filter is the predicate. Both lambdas are short, single-use functions that exist only to customize the behavior of map and filter. This is the ideal use case for lambda expressions, as discussed in the article on Python lambda functions explained. When the transformation or predicate is more complex, a named def function can be passed instead, and map and filter work identically either way.

Writing your own higher-order functions

Any function you write that accepts a callable argument is a higher-order function. The pattern is to identify the fixed part of an algorithm and the variable part, implement the fixed part as a loop or conditional structure, and accept the variable part as a function parameter. This turns a single-purpose function into a reusable one that can be adapted to different needs:

pythonpython
def apply_to_each(func, items):
    result = []
    for item in items:
        result.append(func(item))
    return result
 
doubled = apply_to_each(lambda x: x * 2, [1, 2, 3])

The apply_to_each function handles the iteration and collection. The func parameter handles the transformation. This is essentially a simplified version of map, and writing it yourself reveals how little magic is involved. The function parameter func is called exactly like any other function; the only difference is that its identity is determined by the caller rather than hardcoded in the implementation.

Higher-order functions are especially useful when the fixed part of the algorithm is complex and the variable part is simple. A function that traverses a file system tree, for example, might accept a callback that is called on each file found. The traversal logic, handling directories, symlinks, and error conditions, is encapsulated in the higher-order function. The callback that processes each file is supplied by the caller and can be as simple as printing the file name or as complex as analyzing the file's contents.

Higher-order functions that return functions

A function that returns another function is also a higher-order function. This is the factory pattern from the closures in Python article, where an outer function creates and returns an inner function customized with the outer function's parameters. The returned function is a value, and the caller can store it, call it, or pass it to yet another higher-order function:

pythonpython
def multiply_by(factor):
    def multiply(x):
        return x * factor
    return multiply
 
double = multiply_by(2)
triple = multiply_by(3)

The multiply_by function is higher-order because it returns a function. The returned multiply function is a closure that remembers the factor from multiply_by's scope. Each call to multiply_by produces a new function with a different captured factor. This pattern creates families of related functions from a single template, which is more maintainable than writing each variant by hand.

When a function both accepts and returns functions, it sits at the intersection of both higher-order patterns. Decorators are the canonical example: a decorator accepts a function, wraps it with additional behavior, and returns the wrapped version. The decorator is a higher-order function in both directions, and understanding higher-order functions is prerequisite to understanding how decorators work under the hood.

Rune AI

Rune AI

Key Insights

  • A higher-order function accepts a function as an argument, returns a function, or both.
  • Python's built-in map, filter, and sorted are higher-order functions that customize behavior through function arguments.
  • Higher-order functions separate the iteration or orchestration logic from the specific operation being performed.
  • Writing your own higher-order functions makes code reusable across different transformations without duplication.
  • List comprehensions are often preferred over map and filter in Python, but the pattern of higher-order functions remains valuable.
RunePowered by Rune AI

Frequently Asked Questions

What is a higher-order function in Python?

A higher-order function is a function that either accepts another function as an argument, returns a function as its result, or both. Python's built-in map, filter, and sorted are higher-order functions because they accept a function argument. Decorators are higher-order functions because they both accept and return functions.

Should I use map and filter or list comprehensions?

Many Python developers prefer list comprehensions for readability. The expression [x * 2 for x in numbers] is often considered clearer than list(map(lambda x: x * 2, numbers)). However, map and filter can be more readable when the transformation function already has a name: list(map(str.upper, words)) reads well. Use whichever form is clearer in context.

Can I write my own higher-order functions?

Yes. Any function that accepts a callable as a parameter or returns a callable is a higher-order function. Common examples include functions that apply a transformation to each element of a collection, functions that filter items based on a predicate, and factory functions that return customized functions based on configuration.

Conclusion

Higher-order functions are not an exotic concept; they are a practical tool that Python's own built-in functions demonstrate. Accepting a function as an argument lets you write functions that adapt their behavior to the caller's needs without knowing those needs in advance. Returning a function lets you write factories that produce customized callables. Both patterns build on the first-class nature of Python functions and appear throughout idiomatic Python code.