📝 Python

Function Composition — Build Your Own LEGO! 🧩

0
Author
04e5cc8b-58ac-4bdc-bdee-661bbb
📅
Published
03.04.2026
⏱️
Reading time
7 min
👁️
Views
103
🌱
Level
Beginner

What Is Function Composition?

Function Composition is the practice of combining simple functions into more complex ones.

In Math:

(f ∘ g)(x) = f(g(x))

Read as: “f after g” — first g, then f.

In Python:

def add_10(x):
    return x + 10

def multiply_2(x):
    return x * 2

# Composition: first add_10, then multiply_2
result = multiply_2(add_10(5))
print(result)  # 30
# 5 → add_10 → 15 → multiply_2 → 30

Functions are like LEGO bricks — build complex things from simple pieces! 🧱


Why Use Composition?

Without composition (repetition):

# Processing data in 3 steps
data1 = step1(raw_data)
data2 = step2(data1)
result = step3(data2)

# We repeat this every time!
data1 = step1(other_data)
data2 = step2(data1)
result = step3(data2)

With composition (reuse):

# Build the pipeline once
process = compose(step1, step2, step3)

# Use it many times!
result1 = process(raw_data)
result2 = process(other_data)
result3 = process(more_data)

Simple compose() Function

def compose(f, g):
    """Compose two functions: f(g(x))."""
    def composed(x):
        return f(g(x))
    return composed

# Example
def add_10(x):
    return x + 10

def multiply_2(x):
    return x * 2

# Create a new function
add_then_multiply = compose(multiply_2, add_10)

print(add_then_multiply(5))  # 30
# 5 → add_10 → 15 → multiply_2 → 30

Composing Many Functions

def compose(*functions):
    """Compose multiple functions."""
    def composed(x):
        result = x
        for func in reversed(functions):  # Right to left!
            result = func(result)
        return result
    return composed

# Example
def add_10(x):
    return x + 10

def multiply_2(x):
    return x * 2

def square(x):
    return x ** 2

# Composition: first add_10, then multiply_2, then square
pipeline = compose(square, multiply_2, add_10)

print(pipeline(5))  # 900
# 5 → add_10 → 15 → multiply_2 → 30 → square → 900

Reads right to left! (as in mathematics)


pipe — Composition Left to Right

def pipe(*functions):
    """Apply functions left to right."""
    def piped(x):
        result = x
        for func in functions:  # Left to right!
            result = func(result)
        return result
    return piped

# Example (same result)
def add_10(x):
    return x + 10

def multiply_2(x):
    return x * 2

def square(x):
    return x ** 2

# Pipe: add_10 → multiply_2 → square (reads naturally!)
pipeline = pipe(add_10, multiply_2, square)

print(pipeline(5))  # 900
# 5 → add_10 → 15 → multiply_2 → 30 → square → 900

Reads left to right! (more natural for code)


Practical Examples

Example 1: String Processing

def trim(text):
    """Strip whitespace."""
    return text.strip()

def lowercase(text):
    """Convert to lowercase."""
    return text.lower()

def remove_punctuation(text):
    """Remove punctuation marks."""
    import string
    return text.translate(str.maketrans("", "", string.punctuation))

# Composition
clean_text = pipe(trim, lowercase, remove_punctuation)

# Usage
dirty = "  Hello, World!  "
clean = clean_text(dirty)
print(clean)  # "hello world"

Example 2: Number Processing

def validate_positive(x):
    """Ensure value is > 0."""
    if x <= 0:
        raise ValueError("Must be positive")
    return x

def apply_discount(percent):
    """Apply a percentage discount."""
    def discount(price):
        return price * (1 - percent / 100)
    return discount

def add_tax(percent):
    """Add a tax percentage."""
    def tax(price):
        return price * (1 + percent / 100)
    return tax

def round_price(price):
    """Round to 2 decimal places."""
    return round(price, 2)

# Composition: validate → 20% discount → 10% tax → round
calculate_price = pipe(
    validate_positive,
    apply_discount(20),
    add_tax(10),
    round_price
)

# Usage
base_price = 100
final_price = calculate_price(base_price)
print(final_price)  # 88.0
# 100 → validate → 80 (discount) → 88 (tax) → 88.0 (rounded)

Example 3: List Processing

def filter_even(numbers):
    """Keep only even numbers."""
    return [x for x in numbers if x % 2 == 0]

def double_all(numbers):
    """Double every element."""
    return [x * 2 for x in numbers]

def sum_all(numbers):
    """Sum all elements."""
    return sum(numbers)

# Composition
process_numbers = pipe(filter_even, double_all, sum_all)

# Usage
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = process_numbers(numbers)
print(result)  # 60
# [1..10] → [2,4,6,8,10] → [4,8,12,16,20] → 60

Composition with Decorators

Decorators are composition too!

def uppercase_decorator(func):
    """Decorator: convert result to uppercase."""
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def exclaim_decorator(func):
    """Decorator: append exclamation marks."""
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return f"{result}!!!"
    return wrapper

@exclaim_decorator
@uppercase_decorator
def greet(name):
    return f"hello, {name}"

print(greet("Alice"))  # "HELLO, ALICE!!!"
# greet → uppercase → exclaim

Partial Application

Creating specialized functions from general ones.

from functools import partial

def power(base, exponent):
    """Raise base to exponent."""
    return base ** exponent

# Create specialized functions
square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(5))    # 125

# Composition with partial
def add(a, b):
    return a + b

add_10 = partial(add, 10)
add_100 = partial(add, 100)

process = pipe(add_10, add_100)
print(process(5))  # 115 (5 + 10 + 100)

Currying

Transforming a multi-argument function into a chain of single-argument functions.

def curry(func):
    """Turn a function into a curried version."""
    def curried(a):
        def inner(b):
            return func(a, b)
        return inner
    return curried

# Regular function
def add(a, b):
    return a + b

# Curried
curried_add = curry(add)

# Usage
add_10 = curried_add(10)
print(add_10(5))   # 15
print(add_10(20))  # 30

# Composition with currying
add_5 = curried_add(5)
add_10 = curried_add(10)

add_both = pipe(add_5, add_10)
print(add_both(100))  # 115 (100 + 5 + 10)

Composition for AI Processing

Model Output Pipeline

def normalize(data):
    """Normalize to 0-1 range."""
    max_val = max(data) if data else 1
    return [x / max_val for x in data]

def apply_threshold(threshold):
    """Zero out values below the threshold."""
    def thresholded(data):
        return [x if x >= threshold else 0 for x in data]
    return thresholded

def to_classes(data):
    """Convert to binary classes 0/1."""
    return [1 if x > 0.5 else 0 for x in data]

def count_positives(data):
    """Count positive predictions."""
    return sum(data)

# Composition: normalize → threshold 0.3 → classify → count
process_model_output = pipe(
    normalize,
    apply_threshold(0.3),
    to_classes,
    count_positives
)

# Usage
raw_scores = [45, 78, 23, 89, 56, 12, 90]
positive_count = process_model_output(raw_scores)
print(positive_count)  # 4
# [45,78,23,89,56,12,90] → [0.5,0.87,0.26,0.99,0.62,0.13,1.0]
# → [0.5,0.87,0,0.99,0.62,0,1.0] → [0,1,0,1,1,0,1] → 4

The Composable Class

A convenient class for composition.

class Composable:
    """A function with a compose method."""

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def then(self, other):
        """Compose: self first, then other."""
        def composed(*args, **kwargs):
            return other(self(*args, **kwargs))
        return Composable(composed)

# Usage
@Composable
def add_10(x):
    return x + 10

@Composable
def multiply_2(x):
    return x * 2

@Composable
def square(x):
    return x ** 2

# Method chaining!
pipeline = add_10.then(multiply_2).then(square)

print(pipeline(5))  # 900
# 5 → add_10 → 15 → multiply_2 → 30 → square → 900

Debugging Composition

Execution Tracing

def trace(func):
    """Decorator for debugging."""
    def wrapper(x):
        result = func(x)
        print(f"{func.__name__}({x}) = {result}")
        return result
    wrapper.__name__ = func.__name__
    return wrapper

# Apply trace
@trace
def add_10(x):
    return x + 10

@trace
def multiply_2(x):
    return x * 2

@trace
def square(x):
    return x ** 2

# Composition
pipeline = pipe(add_10, multiply_2, square)

print(pipeline(5))
# Output:
# add_10(5) = 15
# multiply_2(15) = 30
# square(30) = 900
# 900

Common Mistakes

Mistake 1: Wrong Function Order

def add_10(x):
    return x + 10

def multiply_2(x):
    return x * 2

# compose reads right to left!
wrong = compose(add_10, multiply_2)
print(wrong(5))  # 20 (5*2 + 10), NOT 30!

# pipe reads left to right!
right = pipe(add_10, multiply_2)
print(right(5))  # 30 ((5+10) * 2)

Mistake 2: Functions with Incompatible Signatures

def add(a, b):  # 2 arguments!
    return a + b

def square(x):  # 1 argument!
    return x ** 2

# ❌ ERROR
pipeline = pipe(add, square)
# square expects 1 argument, but add returns a number

# ✅ CORRECT: use partial or lambda
from functools import partial

add_10 = partial(add, 10)  # Now takes 1 argument
pipeline = pipe(add_10, square)
print(pipeline(5))  # 225 ((5+10)^2)

Mistake 3: Side Effects

counter = 0

def increment_and_double(x):
    global counter
    counter += 1  # Side effect!
    return x * 2

# Composing functions with side effects — BAD!
# Results become unpredictable

Summary

Function Composition — what it is:

  • ✅ Combining simple functions into complex ones
  • ✅ Reusing logic
  • ✅ Readable pipelines
  • ✅ Pure functions without side effects

Two approaches:

# compose: right to left (math convention)
compose(f, g, h)(x) → f(g(h(x)))

# pipe: left to right (code convention)
pipe(h, g, f)(x) → f(g(h(x)))

compose implementation:

def compose(*functions):
    def composed(x):
        result = x
        for func in reversed(functions):
            result = func(result)
        return result
    return composed

pipe implementation:

def pipe(*functions):
    def piped(x):
        result = x
        for func in functions:
            result = func(result)
        return result
    return piped

Usage:

# Define the steps
def step1(x): return x + 10
def step2(x): return x * 2
def step3(x): return x ** 2

# Build the pipeline
process = pipe(step1, step2, step3)

# Run it
result = process(5)  # 900

What’s Next?

Now you know Function Composition! 🎉

Applications:
- 🏭 Data pipelines — chained data processing
- 🤖 AI workflows — processing model outputs
- 🧹 Data cleaning — composing filters
- 🔄 Transformations — multi-step conversions

Related topics:
- Decorators — wrappers for functions
- Higher-order functions
- Functional programming paradigm

Function composition is the foundation of elegant code! 🧩✨

Your reaction to the article

💬 Comments (0)

🔐 Sign in to leave a comment
🚪 Login
💭

No comments yet

Be the first to share your opinion about this article!

🔗 Similar

Similar articles

Continue learning with these materials

📝

Setting Up Your Environment: Python, pip, and VS …

Before writing code locally, you need to set up three tools: Python, pip, and VS...

📅 04.06.2026 👁️ 17
📝

The datetime Module: Working with Dates and Times

datetime is Python's standard module for working with dates and times. It's part of the...

📅 08.05.2026 👁️ 67
📝

.env Files and Environment Variables: Keeping Sec…

Imagine you wrote a program with an API key hardcoded in the source and pushed...

📅 08.05.2026 👁️ 76

Did you like the article?

Subscribe to our updates and receive new articles first. Grow with PyLand!