Skip to Content
Course content

331: Function Composition Patterns in Python

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You’ve probably run into this a dozen times: you have a few small, focused functions that each do one thing well, and you need to run a piece of data through all of them in a specific order. The instinct for most of us is to just nest them. It feels direct. It’s "just Python." But as the chain grows, the code starts to look more like a Matryoshka doll than a professional codebase.

The Matryoshka Doll Problem

Imagine we're building a system to sanitize user-submitted usernames before they hit the database. We need to strip whitespace, lowercase everything, and then remove any characters that aren't alphanumeric. In a naive implementation, it looks like this:

def strip_space(text):
    return text.strip()

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

def alpha_numeric(text):
    return "".join(char for char in text if char.isalnum())

# The naive approach
username = "  User_Name_123!  "
cleaned = alpha_numeric(lowercase(strip_space(username)))

Now, for three functions, this is tolerable. But I've seen this pattern grow to six or seven levels of nesting in production code. When you're reading this, you have to read from the inside out—right to left—which is the opposite of how we read English. If I tell you to add a "length check" step between lowercasing and alphanumeric filtering, you have to carefully peel back a layer of parentheses, insert the function, and hope you didn't break the nesting. It's brittle and a headache to debug.

Creating a Formal Composition Pipeline

I prefer to treat function composition as a first-class citizen. Instead of nesting calls, we can create a compose utility. The goal is to pass a list of functions and a value, and have Python handle the "piping" for us. We can use functools.reduce to achieve this elegantly.

from functools import reduce

def compose(*funcs):
    """Returns a function that is the composition of the given functions."""
    return lambda x: reduce(lambda v, f: f(v), funcs, x)

# Now we define our pipeline as a reusable object
sanitize_username = compose(
    strip_space,
    lowercase,
    alpha_numeric
)

username = "  User_Name_123!  "
print(sanitize_username(username)) # "username123"

See the difference? Now the logic reads top-to-bottom. If you need to add a new step, you just add a new function to the compose arguments. I love this because it separates the what (the sequence of transformations) from the how (the act of calling them). You've essentially created a domain-specific pipeline for your data.

The Trade-off: Abstraction vs. Transparency

I should be honest with you: this isn't always the right move. If you're only combining two functions once in your entire app, compose is overkill. You're introducing a lambda and a reduce call where a simple nested call would have sufficed. You're adding a layer of abstraction that a junior dev might find confusing if they aren't familiar with functional programming patterns.

However, the moment you find yourself writing "wrapper" functions that do nothing but call other functions in a sequence, you've hit the tipping point. At that stage, the clarity of a pipeline outweighs the "magic" of the compose utility. It turns your data transformation into a configuration problem rather than a logic problem, which is where you want to be as your system scales.




📋 Practical Task

Build a Log Message Sanitization Pipeline

You are tasked with cleaning up raw system logs before they are sent to an analytics dashboard. The logs are messy and contain sensitive information that must be removed.

Requirements:

  • Create three separate functions:
    1. remove_timestamp: Removes everything before the first space (assuming the timestamp is always the first word).
    2. mask_emails: Replaces any string containing an "@" symbol with the string "[EMAIL]".
    3. truncate_log: Ensures the final string is no longer than 50 characters, appending "..." if it is.
  • Implement a compose function (using reduce) to combine these three steps.
  • Apply the resulting pipeline to the following test string: "2023-10-12 14:00:01 Error: User admin@company.com failed to upload a very large file to the server".

Expected Output: "Error: User [EMAIL] failed to upload a very l..."

Rating
0 0

There are no comments for now.

to be the first to leave a comment.