Skip to main content

How to Use Function Composition in Python

Function composition is a technique where two or more functions are combined so that the output of one function becomes the input of the next. It's a foundational concept in functional programming that enables you to build complex operations from small, reusable, single-purpose functions.

In mathematical notation, composing functions f and g is written as f(g(x)) - first apply g to x, then apply f to the result. Python makes this pattern easy to implement, even though it's not a purely functional language.

This guide covers everything from basic composition to advanced techniques like composing multiple functions, handling multiple arguments, and using decorators.

Basic Function Composition

The simplest form of function composition is calling one function inside another:

def add_two(x):
return x + 2

def double(x):
return x * 2

# Compose: double(add_two(5)) → double(7) → 14
result = double(add_two(5))
print(result)

Output:

14
note

add_two(5) returns 7, which is then passed to double(), producing 14. The data flows from the innermost function outward.

note

While this works, manually nesting function calls becomes hard to read as you add more functions: f(g(h(k(x)))). The following sections show cleaner approaches.

Creating a Reusable compose Function

Instead of manually nesting calls, you can create a utility function that composes any two functions into a new function:

def compose(f, g):
"""Return a new function that computes f(g(x))."""
return lambda x: f(g(x))

def add_two(x):
return x + 2

def double(x):
return x * 2

# Create a composed function
double_after_add = compose(double, add_two)

print(double_after_add(5))
print(double_after_add(10))

Output:

14
24

The compose() function returns a new function (via lambda) that applies g first, then f. This composed function can be stored, reused, and passed around like any other function.

Reading order

In compose(f, g), the functions are applied right to left: g runs first, then f. This matches the mathematical convention f ∘ g (read as "f after g"). Keep this in mind to avoid confusion about execution order.

Composing Multiple Functions

Real-world scenarios often require chaining more than two functions. Using functools.reduce(), you can compose an arbitrary number of functions into a single pipeline:

from functools import reduce

def compose(*functions):
"""Compose multiple functions: compose(f, g, h)(x) = f(g(h(x)))."""
return reduce(lambda f, g: lambda x: f(g(x)), functions)

# Define simple, single-purpose functions
def add_two(x):
return x + 2

def subtract_one(x):
return x - 1

def double(x):
return x * 2

# Compose them: double(subtract_one(add_two(x)))
pipeline = compose(double, subtract_one, add_two)

print(pipeline(5))

Output:

12

Execution Order Breakdown

Step 1: add_two(5)      → 7
Step 2: subtract_one(7) → 6
Step 3: double(6) → 12

Functions are applied right to left - the last argument to compose() runs first.

Left-to-Right Composition (Pipe)

If you prefer a left-to-right reading order (often called "piping"), simply reverse the function list:

from functools import reduce

def pipe(*functions):
"""Pipe functions left to right: pipe(f, g, h)(x) = h(g(f(x)))."""
return reduce(lambda f, g: lambda x: g(f(x)), functions)

def add_two(x):
return x + 2

def subtract_one(x):
return x - 1

def double(x):
return x * 2

# Read left to right: add_two → subtract_one → double
pipeline = pipe(add_two, subtract_one, double)

print(pipeline(5))

Output:

12

This produces the same result but reads more naturally: "add two, then subtract one, then double."

Handling Multiple Arguments

Standard function composition expects each function to take a single argument. When you need to handle multiple arguments, there are a few strategies.

Using Tuples to Pass Multiple Values

def process_inputs(x, y):
return (x * 2, y * 2)

def sum_values(args):
return args[0] + args[1]

def composed(x, y):
return sum_values(process_inputs(x, y))

print(composed(3, 4))

Output:

14

Allowing the First Function to Accept Multiple Arguments

A more flexible approach modifies the compose utility to handle *args and **kwargs for the first function in the chain:

def compose(*functions):
"""Compose functions, allowing the innermost to accept multiple arguments."""
def composed(*args, **kwargs):
# Apply the last function with the original arguments
result = functions[-1](*args, **kwargs)
# Apply remaining functions right to left
for func in reversed(functions[:-1]):
result = func(result)
return result
return composed

def add(x, y):
return x + y

def double(x):
return x * 2

def to_string(x):
return f"Result: {x}"

pipeline = compose(to_string, double, add)

print(pipeline(3, 4))

Output:

Result: 14

Composing with Different Data Types

Function composition isn't limited to numbers. You can compose functions that transform strings, lists, or any data type, as long as each function's output is compatible with the next function's input:

def compose(f, g):
return lambda x: f(g(x))

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

def shout(text):
return text.upper()

welcome = compose(shout, greet)

print(welcome("alice"))

Output:

HELLO, ALICE!
note

greet("alice") produces "Hello, alice!", which shout() converts to uppercase.

Function Composition with Decorators

Python decorators are a natural application of function composition. When you stack decorators, you're composing their behaviors - each decorator wraps the function with additional logic.

import time
from functools import wraps

def log_call(func):
"""Decorator that logs function calls."""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
return func(*args, **kwargs)
return wrapper

def measure_time(func):
"""Decorator that measures execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f} seconds")
return result
return wrapper

@log_call
@measure_time
def add(x, y):
return x + y

result = add(3, 4)
print(f"Result: {result}")

Output:

Calling add((3, 4), {})
add took 0.000001 seconds
Result: 7
Use functools.wraps

Always use @wraps(func) in your decorator's wrapper function. Without it, the decorated function loses its original __name__, __doc__, and other metadata:

from functools import wraps

def my_decorator(func):
@wraps(func) # Preserves func.__name__, func.__doc__, etc.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

Practical Example: Data Processing Pipeline

Function composition is especially powerful for data transformation pipelines where data flows through a series of processing steps:

from functools import reduce

def pipe(*functions):
return reduce(lambda f, g: lambda x: g(f(x)), functions)

# Define individual transformation steps
def remove_whitespace(text):
return text.strip()

def lowercase(text):
return text.lower()

def replace_spaces(text):
return text.replace(" ", "_")

def add_prefix(text):
return f"user_{text}"

# Build the pipeline
normalize_username = pipe(
remove_whitespace,
lowercase,
replace_spaces,
add_prefix
)

# Apply to different inputs
print(normalize_username(" John Doe "))
print(normalize_username("Jane Smith"))
print(normalize_username(" BOB "))

Output:

user_john_doe
user_jane_smith
user_bob

Each function handles one responsibility, and the pipeline composes them into a complete transformation.

Benefits of Function Composition

BenefitDescription
ModularityBreak complex logic into small, focused functions
ReusabilityEach function can be used independently or in different compositions
TestabilitySmall functions are easier to unit test
ReadabilityPipelines clearly show the data transformation flow
No side effectsPure functions produce predictable, composable behavior

Conclusion

Function composition is a powerful technique for building complex behavior from simple, reusable functions.

  • In Python, you can compose functions by nesting calls, creating a reusable compose() utility, or building left-to-right pipelines with pipe().
  • For multi-function chains, functools.reduce() makes composition scalable.
  • Decorators provide a Pythonic way to apply composition patterns to enhance existing functions.

By embracing composition, you write code that is more modular, testable, and maintainable.