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:
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:
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:
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:
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
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.
Frequently Asked Questions
What is a higher-order function in Python?
Should I use map and filter or list comprehensions?
Can I write my own higher-order functions?
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.
More in this topic
Define and Call Python Functions
Learn the exact syntax for defining and calling Python functions with the def keyword, including naming rules, the function body, and how the call stack works.
Python Function Parameters and Arguments
Learn the difference between parameters and arguments, how to pass values by position and by keyword, and the rules for default parameter values.
Python Functions Explained
Learn what Python functions are, why they matter for organizing code, and how they form the foundation of non-trivial programs.