Skip to 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