AlgoMaster Logo

map, filter & reduce

High Priority16 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

map, filter, and reduce are Python's three classic higher-order functions for transforming, selecting, and collapsing data from an iterable. They predate list comprehensions and still show up in real code, especially when the operation is a single named function. This lesson covers how each one works, the lazy iterator behavior in Python 3, the multi-iterable form of map, when to prefer comprehensions, and how composing the three reads in practice.

map: Transform Every Item

map(func, iterable) calls func on every item in iterable and yields the results one at a time. It's the answer to the question "I have a list of things, give me a list of transformed things."

A typical use: a list of cart prices in dollars, and you want them in cents (because you decided to store everything as integers to avoid floating-point trouble).

Two things to notice. First, map doesn't return a list, it returns a map object, which is a lazy iterator. Nothing actually runs until something consumes it (here, list(...)). Second, the function gets called once per item, with that item as its only argument.

map is at its cleanest when the transformation already exists as a named function. Suppose the catalog has product names from a sloppy import and you want them title-cased:

Here str.title is the function. You don't need a lambda because the method already takes a string and returns a string. This is the form where map reads better than a comprehension: map(str.title, names) is shorter and clearer than [name.title() for name in names]. Pick whichever your team finds easier to read; both are fine.

map With Multiple Iterables

map accepts more than one iterable. When you pass n iterables, the function must accept n arguments, and map calls it with one item from each iterable per step. It stops as soon as the shortest iterable runs out.

Picture two parallel lists from a database query: unit prices and quantities. You want the line total for each row.

Each call gets a price and a qty, and you get one result per pair. If quantities had only three items, map would stop after three results without complaining, even though unit_prices has four. That silent truncation surprises people; if you want a loud error when the lengths don't match, use zip(..., strict=True) (Python 3.10+) instead.

For two-iterable transformations like this, many teams prefer zip plus a comprehension, because it spells out the pairing:

Both produce the same list. Use whichever is clearer at the call site.

filter: Keep What Matches

filter(func, iterable) keeps items for which func(item) is truthy and drops the rest. Like map, it returns a lazy iterator, not a list.

A common need: out of all products in a category, keep only the ones in stock.

The predicate (lambda p: p["stock"] > 0) returns a bool for each product, and filter keeps the ones where the bool is true. Items where the predicate returns False, None, 0, "", or any other falsy value are dropped.

filter(None, iterable)

There's a special form: if the first argument is None, filter keeps items that are truthy on their own. This is the idiom for stripping empty strings, None values, or zeros from a list without writing a lambda.

Anything that's "falsy" gets dropped. That includes empty strings, None, 0, 0.0, empty lists, and empty dicts. Use this only when "falsy" is exactly what you want; otherwise write the explicit predicate so the intent is obvious.

reduce: Collapse Into a Single Value

reduce(func, iterable, initial) lives in functools, not as a built-in. Guido moved it out in Python 3 because for sum, product, max, and min, the built-ins are clearer. But for "collapse a sequence into one value using a custom operation", reduce is still the cleanest tool.

reduce walks the iterable left to right, carrying an accumulator. Each step calls func(accumulator, item) and the result becomes the new accumulator. After the last item, it returns the final accumulator.

The mechanic, in words: pick the first two items, combine them, then combine that with the third, then with the fourth, and so on.

This is also exactly sum(cart_totals), and sum is what you should use in real code. reduce earns its keep when the combining operation isn't a built-in.

Here's a more honest example. Each order has a discount expressed as a multiplier (0.9 means 10% off). Stack them by multiplying:

There's no built-in product() in plain Python (it's in math.prod from 3.8+ for numbers). For arbitrary combining operations, like merging dictionaries or chaining function calls, reduce is the right shape.

The diagram shows the running accumulator: combine the first two values into an intermediate result, then combine that with the next value, and so on. Each step shrinks the working set by one. After processing every item, you're left with a single value.

The initial Argument

reduce has an optional third argument: an initial value for the accumulator. Three things change when you pass it:

  1. The accumulator starts at initial instead of the first item.
  2. func gets called once for every item in the iterable (instead of n - 1 times).
  3. An empty iterable returns initial instead of raising TypeError.

That third point is the practical one. Without an initial value, reduce on an empty list crashes.

For any reduction where the input could be empty, pass an initial value. It's also how you reduce into a different type than the items themselves: start with {} and reduce into a dictionary, start with [] and reduce into a list (though for those, a loop or dict.update() is usually clearer).

Lazy Iterators in Python 3

In Python 2, map and filter returned lists. In Python 3, they return lazy iterators. That change matters for three reasons.

One: you can chain them on huge inputs without allocating intermediate lists. Filtering a 10-million-row product log and then mapping each match to a summary doesn't build a 10-million-row temporary list, it walks the records one at a time.

Two: you can only iterate the result once. A map object is exhausted after one pass.

The second list(...) gets nothing because the iterator is already drained. If you need to iterate twice, materialize once with list(...) and reuse the list.

Three: nothing runs until something consumes the iterator. The lambda inside map isn't called when you write map(...), only when the result is iterated.

The "processing" lines come after "after map call, before consumption", which proves the function only runs when the loop pulls each value.

The diagram shows the lifecycle. Creating the map or filter object is cheap and does no real work. The transformation runs only when something downstream asks for values. This is the same model as generator expressions (covered in §17 Comprehensions).

map/filter vs Comprehensions

For most code, list and generator comprehensions (see §17) are the more Pythonic choice. They keep the transformation and the source close together, they read in roughly the order you'd say them out loud, and they don't need a lambda for simple expressions.

OperationWith map/filterWith comprehension
Apply a functionlist(map(str.title, names))[n.title() for n in names]
Multiply each itemlist(map(lambda p: p * 100, prices))[p * 100 for p in prices]
Keep matching itemslist(filter(lambda p: p["stock"] > 0, products))[p for p in products if p["stock"] > 0]
Drop falsy itemslist(filter(None, emails))[e for e in emails if e]
Transform and filterlist(map(f, filter(g, items)))[f(x) for x in items if g(x)]

The opinionated answer most Python developers reach for: use a comprehension by default. Use `map` or `filter` when the function is already a named function and there's no extra expression around it.

So map(str.lower, names) is fine because str.lower is already a clean named callable. But map(lambda p: p["price"] * 0.9, products) reads better as [p["price"] * 0.9 for p in products]. The lambda adds visual noise that the comprehension doesn't have.

For lazy evaluation, both are available: map/filter are lazy by default, and the comprehension version is (x * 2 for x in items) (parentheses, not brackets), which is a generator expression.

Composing map and filter

You can stack map and filter to build a pipeline: filter the records you care about, then transform them.

Read it from inside out: take products, keep the ones in stock, multiply each price by 0.9, sum the result. Because both filter and map are lazy, no intermediate list is built. The whole pipeline streams one product at a time through the chain.

The same pipeline as a comprehension:

That one line beats the three-line chain for almost any reader. Once your pipeline grows past two stages, comprehensions usually win on clarity. map/filter chains start to feel cluttered when there are three or more lambdas in a row.

There's a real readability cliff here. A single map(str.lower, names) is great. A chain of three lambdas piped through map and filter is harder to read than the equivalent comprehension or a small named function with a for loop. When in doubt, write it both ways and pick the one a teammate would understand faster.

When reduce Is Genuinely the Right Tool

reduce has the reputation of being clever when it shouldn't be. For sums, products, mins, maxes, and concatenations, the built-ins (sum, math.prod, min, max, "".join) are clearer and faster.

But reduce earns its place for custom combining operations that aren't already a built-in. Two common shapes:

Stacking dictionaries. Suppose a customer profile is assembled from defaults, account settings, and per-session overrides, each as a dict. Later dicts win on conflicts.

Each step takes the running merged dict and the next override, builds a new dict with the later keys winning. This reads cleanly as "fold these dictionaries together with right-wins-on-conflict." A loop would work too, but reduce makes the "fold" intent explicit.

Finding a running summary. Compute the highest-value cart out of a list of carts:

This is also max(carts, key=lambda c: c["total"]), which is shorter and more idiomatic. The reduce version is a good example of the trap: it works, but the built-in max with key= says exactly what's happening with half the noise.

The rule of thumb: if sum, max, min, math.prod, or "".join covers the operation, use them. Reach for reduce when the combining operation is custom (merging dicts, applying a chain of transformations, building a structured result).

Quiz

map, filter, reduce Quiz

10 quizzes