Skip to content

Function composition

The simplest case — combining two functions into one:

# f ∘ g means: apply g first, then f
def compose(f, g):
return lambda x: f(g(x))
double = lambda x: x * 2
add_one = lambda x: x + 1
double_then_add = compose(add_one, double) # add_one(double(x))
add_then_double = compose(double, add_one) # double(add_one(x))
print(double_then_add(5)) # 11 → double(5)=10, then add_one(10)=11
print(add_then_double(5)) # 12 → add_one(5)=6, then double(6)=12

Order matters — compose(f, g) applies g first and f second, just like mathematical notation.


Two directions are possible — right to left (mathematical convention) and left to right (pipeline convention). Both use reduce() to chain an arbitrary number of functions together:

from functools import reduce
def compose_all(*funcs):
"""Right to left: compose_all(f, g, h)(x) = f(g(h(x)))"""
return reduce(compose, funcs)
def pipe_all(*funcs):
"""Left to right: pipe_all(f, g, h)(x) = h(g(f(x)))"""
return reduce(lambda f, g: lambda x: g(f(x)), funcs)

pipe_all is often more readable in practice — the functions are listed in the order they are applied, which matches how you would describe the transformation in plain English:

normalize = pipe_all(str.strip, str.lower) # strip first, then lowercase
print(normalize(" Hello World ")) # "hello world"

Composition really shows its value when building multi-step data transformation pipelines. Each step is a small, focused function — composition wires them together:

import re
from functools import reduce
def compose(f, g):
return lambda x: f(g(x))
def pipe_all(*funcs):
return reduce(lambda f, g: lambda x: g(f(x)), funcs)
# Each step is small and testable on its own
remove_punctuation = lambda s: re.sub(r"[^\w\s]", "", s)
normalize_whitespace = lambda s: re.sub(r"\s+", " ", s)
# Wire them together into a single reusable function
clean_text = pipe_all(
str.strip, # 1. remove leading/trailing whitespace
str.lower, # 2. lowercase everything
remove_punctuation, # 3. strip punctuation
normalize_whitespace, # 4. collapse multiple spaces into one
)
print(clean_text(" Hello, World!!! ")) # "hello world"

Each step in the pipeline is independently readable and testable. Adding or removing a step is a one-line change. This is the practical payoff of function composition — complexity built from simplicity.