sethserver.com Subscribe
None

Functional Programming in Python: map, filter, reduce, and Beyond | Python Guide

By Seth Black • Updated: March 18, 2026

Python · 3 min read

Like this kind of writing? Get one email a week with notes on startups, AI, and the occasional strong opinion about Python: subscribe to the newsletter.

You already know list comprehensions. You've written [x*x for x in nums if x % 2 == 0] and felt that tiny hit of joy that comes from deleting three lines of boilerplate.

Cool. Now let's see what's behind that door.

Python isn't Haskell. It doesn't want to be. But it does have a functional toolbox that most people ignore. If you've ever had to process messy data, glue together an ML pipeline, or survive a codebase where "business logic" means "a thousand if statements," these tools help.

Functions are data (yes, really)

The core trick is simple: functions are first-class. You can pass them around like strings or integers. That sounds academic. But when you're staring at a pile of transformations, you want them to work together and be testable.

Here's the basic shape:

def apply_all(x, funcs):
    for f in funcs:
        x = f(x)
    return x

Now your "pipeline" is just a list of functions. Nothing fancy. Just clean.

map() and filter() are fine. Stop fighting them.

People love to argue about whether map() is "more readable" than a comprehension. The only answer that matters is: which one makes the next person less likely to curse your name.

map() applies a function to each item and returns an iterator:

nums = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, nums))
## [1, 4, 9, 16, 25]

filter() keeps items where a predicate returns True:

evens = list(filter(lambda x: x % 2 == 0, range(1, 11)))
## [2, 4, 6, 8, 10]

Two practical notes:

reduce(): sharp knife, use carefully

reduce() folds a list into one value. It lives in functools because Python is politely suggesting you don't use it unless you mean it.

from functools import reduce

product = reduce(lambda x, y: x * y, [1, 2, 3, 4, 5])
## 120

If you can use sum() or max() instead of reduce(), do it. Use reduce() when it makes the code clearer. This is engineering, not a religion.

Lambdas: useful for glue, terrible for logic

Lambdas are perfect for tiny one-off glue. Like sorting dictionaries:

people = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
    {"name": "Charlie", "age": 35},
]

by_age = sorted(people, key=lambda p: p["age"])

If the lambda needs a comment, it needs a name.

partial() is underrated

functools.partial() lets you pre-fill arguments and create a new function. This comes up constantly in real systems. You have a general function, but you want a specialized version without writing wrappers everywhere.

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

square(4)  # 16
cube(3)    # 27

This looks like a parlor trick right up until you're wiring callbacks or config-driven logic and you realize it's the cleanest option.

lru_cache: memoization that saves your afternoon

I've had jobs where reading the manual made me look like a wizard. Same thing here. lru_cache is built-in memoization. It turns "why is this slow?" into "oh, it's fine now."

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

fib(100)  # fast

Recursive Fibonacci without caching is a performance crime. With caching, it's a decent demo and sometimes even useful for dynamic programming problems.

A practical example: transactions over $100, grouped by customer

Let's do something closer to real life: you have transactions and you want totals per customer, but only for transactions over $100.

from functools import reduce

transactions = [
    {"customer": "Alice", "amount": 120},
    {"customer": "Alice", "amount": 60},
    {"customer": "Bob", "amount": 200},
    {"customer": "Bob", "amount": 30},
    {"customer": "Bob", "amount": 30},
    {"customer": "Charlie", "amount": 90},
]

big = filter(lambda t: t["amount"] > 100, transactions)

totals = reduce(
    lambda acc, t: {**acc, t["customer"]: acc.get(t["customer"], 0) + t["amount"]},
    big,
    {},
)

totals  # {'Alice': 120, 'Bob': 200}

Is that easier to read than a loop? Depends on your team. But it is a pure transformation: input in, output out. That's easy to test and easy to reason about.

If you want to avoid the {**acc, ...} copy each time (you probably do), use a loop or defaultdict. Functional style is a tool, not a vow.

The real win: fewer side effects, fewer surprises

The best part of functional-ish Python is predictability. You know what goes in and what comes out.

Pure functions are easier to test. Pipelines are easier to refactor. And small, checkable functions are easier to debug when someone insists their code is "fine" without checking.

If you only take one thing: write small functions that do one thing. Compose them. Name them. Cache them when it matters.

-Sethers

Share this post
Newsletter

One email, once a week.

Notes on databases, systems, and the occasional strong opinion about Python. No spam, unsubscribe anytime.

Seth Black
Written by

Seth Black

Engineer and founder based in Texas. Writes about databases, AI, and running things in production. Embeds in small teams as lead engineer.

More from Python

View all →
Python

OpenAI Bought Astral - and my fav tool uv

Mar 23, 2026
Python

Python Pydantic Validation: Stop Writing Manual Checks

Mar 04, 2026
Python

Python asyncio Recipes

Mar 04, 2026