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:
- They return iterators, so they're lazy. That's good for big inputs. If you want to dive deeper into lazy iteration patterns, check out mastering Python's itertools for efficient data processing.
- If your lambda has a lambda inside it, stop. Use
def.
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