Skip to content

Clarifications

A higher-order function is a function that does at least one of the following:

  • Takes a function as an argument — it receives behaviour as input
  • Returns a function as output — it produces new behaviour as output

The term comes from mathematics, where functions that operate on other functions are called higher-order. In programming, it is one of the foundational ideas of functional programming — treating functions as first-class values that can be passed around, stored, and returned just like any other data.

This is the idea that unlocks most of what makes functional programming powerful. map(), filter(), reduce(), closures, decorators, and functools.partial are all higher-order functions.


The simplest form — passing a function into another function to customise its behaviour:

# apply() takes a function and a value — the function is an argument
def apply(func, value):
return func(value)
print(apply(str.upper, "hello")) # "HELLO"
print(apply(len, "hello")) # 5
print(apply(lambda x: x ** 2, 5)) # 25

This is exactly what map() and filter() do — they accept a function and an iterable, and apply the function to the data:

nums = [1, 2, 3, 4, 5]
# map and filter are higher-order functions
squared = list(map(lambda x: x ** 2, nums)) # [1, 4, 9, 16, 25]
evens = list(filter(lambda x: x % 2 == 0, nums)) # [2, 4]

A function that produces a new function — this is the pattern behind closures and function factories:

# make_multiplier returns a new function configured with a fixed factor
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# the returned functions work independently
nums = [1, 2, 3, 4, 5]
print(list(map(double, nums))) # [2, 4, 6, 8, 10]
print(list(map(triple, nums))) # [3, 6, 9, 12, 15]

The most powerful form — a function that takes a function and returns a modified version of it. This is the pattern behind decorators:

# timer takes a function and returns a new version that measures execution time
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
def slow_sum(n):
return sum(range(n))
timed_sum = timer(slow_sum)
print(timed_sum(1_000_000)) # slow_sum took 0.0312s
# 499999500000

Python’s @ decorator syntax is just a shorthand for this pattern:

@timer # equivalent to: slow_sum = timer(slow_sum)
def slow_sum(n):
return sum(range(n))

A Practical Example — Building a Pipeline

Section titled “A Practical Example — Building a Pipeline”

Higher-order functions compose naturally. Here is a pipeline that applies a list of transformations to data:

def apply_all(funcs, value):
"""Apply a list of functions in sequence to a value."""
result = value
for func in funcs:
result = func(result)
return result
pipeline = [
str.strip,
str.lower,
lambda s: s.replace(" ", "_"),
]
print(apply_all(pipeline, " Hello World ")) # "hello_world"
print(apply_all(pipeline, " Python Tips ")) # "python_tips"

Or more functionally, using reduce():

from functools import reduce
def pipe(value, *funcs):
return reduce(lambda v, f: f(v), funcs, value)
print(pipe(" Hello World ", str.strip, str.lower, lambda s: s.replace(" ", "_")))
# "hello_world"

PatternDescriptionExample
Function as argumentPass behaviour into a functionmap(func, data)
Function as return valueProduce new behaviourmake_multiplier(2)
BothTransform a function into anotherDecorators, lru_cache

The key insight is that functions are just values — they can be passed, returned, stored in lists, and used as dictionary values. Once you see functions this way, the door opens to a much more expressive and composable style of programming.