Skip to main content

Higher-Order Functions in Python

Higher-order functions either take other functions as arguments or return functions. Python treats functions as first-class objects, making higher-order functions straightforward to use.

Functions as arguments

Functions can be passed as arguments to other functions, enabling flexible and reusable code.

Example 1
def apply_operation(x, y, operation):
return operation(x, y)

def add(a, b):
return a + b

def multiply(a, b):
return a * b

print(apply_operation(5, 3, add))
>>> 8
print(apply_operation(5, 3, multiply))
>>> 15

You can pass lambda functions directly.

Example 2
def apply_operation(x, y, operation):
return operation(x, y)

result = apply_operation(10, 5, lambda a, b: a - b)
print(result)
>>> 5

Built-in functions like map(), filter(), and sorted() are higher-order functions.

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

# map() takes a function as first argument
squared = list(map(lambda x: x ** 2, numbers))
print(squared)
>>> [1, 4, 9, 16, 25]

# filter() takes a function as first argument
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)
>>> [2, 4]

# sorted() takes a function as key argument
words = ["apple", "banana", "cherry"]
sorted_words = sorted(words, key=len)
print(sorted_words)
>>> ['apple', 'cherry', 'banana']

Functions as return values

Functions can return other functions, enabling powerful patterns like function factories.

Example 4
def create_multiplier(n):
def multiplier(x):
return x * n
return multiplier

double = create_multiplier(2)
triple = create_multiplier(3)

print(double(5))
>>> 10
print(triple(5))
>>> 15

The returned function "remembers" the values from the enclosing scope.

Example 5
def create_adder(n):
def adder(x):
return x + n
return adder

add_five = create_adder(5)
add_ten = create_adder(10)

print(add_five(3))
>>> 8
print(add_ten(3))
>>> 13

Function factories

Function factories create and return specialised functions based on parameters.

Example 6
def create_validator(min_value, max_value):
def validate(value):
if min_value <= value <= max_value:
return True
return False
return validate

age_validator = create_validator(18, 65)
print(age_validator(25))
>>> True
print(age_validator(70))
>>> False

You can create multiple validators with different criteria.

Example 7
def create_comparator(operator):
if operator == "greater":
return lambda x, y: x > y
elif operator == "less":
return lambda x, y: x < y
elif operator == "equal":
return lambda x, y: x == y
else:
return lambda x, y: False

greater_than = create_comparator("greater")
print(greater_than(5, 3))
>>> True
print(greater_than(3, 5))
>>> False

Decorators

Decorators are a common use of higher-order functions. They modify or extend the behaviour of other functions.

Example 8
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper

@log_calls
def add(a, b):
return a + b

print(add(3, 4))
>>> Calling add with (3, 4), {}
>>> add returned 7
>>> 7

Decorators can accept arguments themselves.

Example 9
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator

@repeat(3)
def greet(name):
return f"Hello, {name}!"

print(greet("Alice"))
>>> ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']

You can use multiple decorators on a single function.

Example 10
def uppercase(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper

def add_exclamation(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + "!"
return wrapper

@add_exclamation
@uppercase
def greet(name):
return f"hello, {name}"

print(greet("alice"))
>>> HELLO, ALICE!

Closures

Closures occur when a nested function references variables from its enclosing scope. This is fundamental to higher-order functions.

Example 11
def outer_function(x):
def inner_function(y):
return x + y
return inner_function

add_five = outer_function(5)
print(add_five(3))
>>> 8

The closure "captures" the variable from the outer scope.

Example 12
def create_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter

counter1 = create_counter()
counter2 = create_counter()

print(counter1())
>>> 1
print(counter1())
>>> 2
print(counter2())
>>> 1

Each closure maintains its own state.

Common higher-order patterns

Higher-order functions enable many useful patterns like function composition and partial application.

Example 13
def compose(f, g):
def composed(x):
return f(g(x))
return composed

def add_one(x):
return x + 1

def multiply_two(x):
return x * 2

add_then_multiply = compose(multiply_two, add_one)
print(add_then_multiply(5))
>>> 12

You can create a timing decorator for performance measurement.

Example 14
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} seconds")
return result
return wrapper

@timer
def slow_function():
time.sleep(0.1)
return "Done"

slow_function()
>>> slow_function took 0.1001 seconds

Higher-order functions work well with error handling.

Example 15
def handle_errors(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error in {func.__name__}: {e}")
return None
return wrapper

@handle_errors
def divide(a, b):
return a / b

print(divide(10, 2))
>>> 5.0
print(divide(10, 0))
>>> Error in divide: division by zero
>>> None

See also