Python map(), filter(), and reduce()
map(), filter(), and reduce() are Python’s core tools for functional-style data processing: instead of writing an explicit loop, you describe a transformation or a condition and let Python apply it across an entire collection. map() transforms every item, filter() keeps only the items that pass a test, and reduce() collapses a whole sequence down into a single value. Understanding them well also makes list comprehensions, generator expressions, and libraries like itertools and pandas much easier to reason about, since they all build on the same idea.
Overview: How map(), filter(), and reduce() work
map() and filter() are built-in functions available everywhere. reduce() used to be a built-in in Python 2, but in Python 3 it was moved into the functools module because Guido van Rossum felt most uses of it are better written as an explicit loop or as sum()/comprehensions. You still need it often enough that it’s worth mastering.
All three take a function as their first argument and apply it across an iterable (a list, tuple, string, range, generator, dict view, etc.). Crucially, map() and filter() do not return lists — they return special lazy iterator objects (a map object and a filter object respectively). No work happens when you call map() or filter(); the function is only applied item-by-item as something pulls values out of the iterator, for example a for loop, list(), or another function that consumes iterables. This laziness means you can build a pipeline of map()/filter() calls over a huge or even infinite stream of data without ever holding the whole thing in memory at once — only one item is being processed at any moment.
reduce(), by contrast, is not lazy: it must consume the entire iterable to produce its single result, because each step depends on the accumulated result of the previous step. Internally, reduce(function, iterable, initializer) works like this: it keeps a running “accumulator” value, starts it at initializer (or the iterable’s first item if no initializer is given), and then repeatedly calls function(accumulator, next_item), replacing the accumulator with the result each time, until the iterable is exhausted.
Syntax
map(function, iterable, *more_iterables)
filter(function_or_None, iterable)
from functools import reduce
reduce(function, iterable, initializer=None)
| Part | Meaning |
|---|---|
function (map) |
Called once per item; its return value becomes the new item. May take multiple arguments if multiple iterables are passed. |
function_or_None (filter) |
Called once per item; the item is kept only if the function returns a truthy value. If None is passed, items are kept when they are themselves truthy. |
function (reduce) |
Takes two arguments (accumulator, item) and returns the new accumulator. |
iterable |
Any iterable: list, tuple, string, range, generator, dict keys/values, etc. |
initializer (reduce) |
Optional starting value for the accumulator. Strongly recommended — protects against errors on empty iterables. |
Both map() and filter() return an iterator, so you typically wrap the call in list(...), tuple(...), or iterate over it directly with a for loop.
Examples
Example 1: map() to transform data
temperatures_celsius = [0, 20, 25, 30, 100]
def celsius_to_fahrenheit(c):
return c * 9 / 5 + 32
temperatures_fahrenheit = list(map(celsius_to_fahrenheit, temperatures_celsius))
print(temperatures_fahrenheit)
temperatures_fahrenheit_lambda = list(map(lambda c: c * 9 / 5 + 32, temperatures_celsius))
print(temperatures_fahrenheit_lambda)
Output:
[32.0, 68.0, 77.0, 86.0, 212.0]
[32.0, 68.0, 77.0, 86.0, 212.0]
map() calls celsius_to_fahrenheit once for each element of temperatures_celsius and collects the results in order. The second call shows the same transformation written with a lambda — a common style when the function is small and used only once. Note every result is a float, because the formula divides with /.
Example 2: filter() to select data
words = ["sun", "moon", "elephant", "cat", "programming", "ai"]
long_words = list(filter(lambda w: len(w) > 3, words))
print(long_words)
def is_palindrome(s):
return s == s[::-1]
palindromes = list(filter(is_palindrome, ["level", "world", "radar", "python", "civic"]))
print(palindromes)
Output:
['moon', 'elephant', 'programming']
['level', 'radar', 'civic']
filter() keeps only the items for which the function returns a truthy value. In the first call, len(w) > 3 filters out "sun", "cat", and "ai". In the second, is_palindrome checks whether a string reads the same forwards and backwards.
Example 3: reduce() to collapse data
from functools import reduce
numbers = [3, 7, 2, 9, 4, 1, 8]
product = reduce(lambda acc, x: acc * x, numbers)
print(product)
maximum = reduce(lambda acc, x: acc if acc > x else x, numbers)
print(maximum)
cart = [
{"price": 12.5, "qty": 2},
{"price": 5.0, "qty": 3},
{"price": 20.0, "qty": 1},
]
total_price = reduce(lambda acc, item: acc + item["price"] * item["qty"], cart, 0)
print(f"Total: ${total_price:.2f}")
Output:
12096
9
Total: $60.00
The first reduce() multiplies every number together to get the product of the whole list. The second finds the maximum by always keeping the larger of the accumulator and the current item (this is illustrative — in real code you’d just use max(numbers)). The third shows a very common real-world pattern: reducing a list of dictionaries down to a single total, using 0 as the initializer so the accumulator starts as a number, not the first cart item.
Example 4: chaining all three lazily
from functools import reduce
sales = [120, -15, 340, 0, 75, -5, 200]
positive_sales = filter(lambda x: x > 0, sales)
doubled = map(lambda x: x * 2, positive_sales)
total = reduce(lambda acc, x: acc + x, doubled, 0)
print(total)
Output:
1470
This is where laziness pays off: filter() and map() don’t build any intermediate lists. As reduce() pulls one value at a time from doubled, that pulls one value from positive_sales, which pulls one value from sales. Only one number exists in memory at any point in the pipeline, no matter how long sales is.
Under the hood
When you call map(f, iterable), Python immediately returns a lightweight map object that just remembers f and an iterator over iterable — it does no work yet. Each time something calls next() on that object (which for loops and list() do automatically), it pulls the next item from the underlying iterator, calls f on it, and returns the result. When the underlying iterable is exhausted, it raises StopIteration, which signals the consumer to stop. filter objects work the same way, except each pulled item is tested with the predicate and simply skipped (not yielded) if the test fails.
reduce(), being eager, runs a loop internally roughly equivalent to:
def reduce(function, iterable, initializer=None):
it = iter(iterable)
if initializer is None:
accumulator = next(it)
else:
accumulator = initializer
for item in it:
accumulator = function(accumulator, item)
return accumulator
Seeing this loop makes clear why an empty iterable with no initializer fails: there is no first value to seed accumulator with.
Common Mistakes
1. Trying to index a map or filter object directly
map() and filter() return iterators, not lists, so they don’t support indexing:
squares = map(lambda x: x ** 2, [1, 2, 3])
print(squares[0])
Output:
TypeError: 'map' object is not subscriptable
Fix it by converting to a list first if you need indexing or repeated access:
squares = list(map(lambda x: x ** 2, [1, 2, 3]))
print(squares[0])
Output:
1
2. Consuming a map/filter iterator twice
Iterators are single-use — once exhausted, they stay empty:
result = map(str.upper, ["a", "b", "c"])
first_pass = list(result)
second_pass = list(result)
print(first_pass)
print(second_pass)
Output:
['A', 'B', 'C']
[]
If you need the data more than once, store it in a list (or re-create the map() call):
values = list(map(str.upper, ["a", "b", "c"]))
first_pass = values
second_pass = values
print(first_pass)
print(second_pass)
Output:
['A', 'B', 'C']
['A', 'B', 'C']
3. Calling reduce() on an empty sequence with no initializer
from functools import reduce
empty_list = []
total = reduce(lambda acc, x: acc + x, empty_list)
print(total)
Output:
TypeError: reduce() of empty iterable with no initial value
Always pass an initializer so the function has a sensible result even when the iterable is empty:
from functools import reduce
empty_list = []
total = reduce(lambda acc, x: acc + x, empty_list, 0)
print(total)
Output:
0
Best Practices
- For simple transformations and filters, many Pythonistas prefer a list/generator comprehension over
map()/filter()with alambda— e.g.[x * 2 for x in nums if x > 0]reads more naturally than nestingmap()insidefilter(). Reach formap()/filter()when you already have a named function to pass in, or when working with existing lazy pipelines. - Prefer a small
def-named function over a complexlambdawhen the logic is more than a one-liner — it documents intent and is easier to debug. - Always import
reduceexplicitly withfrom functools import reduce; it is not a builtin in Python 3. - Give
reduce()aninitializerwhenever the iterable could plausibly be empty, to avoid aTypeErrorat runtime. - Before reaching for
reduce(), check whether a builtin already does the job more clearly:sum(),max(),min(),any(),all(), ormath.prod(). - Remember
map()/filter()results are single-use iterators — wrap them inlist()ortuple()if you need to iterate more than once or needlen()/indexing. - When chaining several lazy steps over large or infinite data, prefer generator expressions or
itertoolsfunctions, which are usually clearer than deeply nestedmap(...filter(...))calls.
Practice Exercises
- Exercise 1: Given
words = ["python", "is", "fun", "and", "powerful"], usemap()to build a list of each word’s length. Expected output:[6, 2, 3, 3, 8]. - Exercise 2: Given
numbers = [-3, 8, -1, 12, 0, 5, -7], usefilter()to build a list containing only the non-negative numbers. Expected output:[8, 12, 0, 5]. - Exercise 3: Given
words = ["cat", "elephant", "dog", "hippopotamus", "ant"], usefunctools.reduce()to find the longest word in a single pass (hint: compare lengths in your reducer function). Expected output:"hippopotamus".
Summary
map(function, iterable)applies a function to every item and returns a lazy iterator of results.filter(function, iterable)keeps only the items for which the function returns a truthy value, also lazily.functools.reduce(function, iterable, initializer)eagerly collapses an iterable into a single accumulated value.map()andfilter()objects are single-use iterators — wrap inlist()/tuple()to reuse or index them.- Always give
reduce()an initializer to avoid errors on empty iterables. - Comprehensions are often clearer than
map()/filter()with alambda; use whichever best communicates intent. - Check for a purpose-built builtin (
sum(),max(),any(), etc.) before reaching forreduce().
