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 a lambda — e.g. [x * 2 for x in nums if x > 0] reads more naturally than nesting map() inside filter(). Reach for map()/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 complex lambda when the logic is more than a one-liner — it documents intent and is easier to debug.
  • Always import reduce explicitly with from functools import reduce; it is not a builtin in Python 3.
  • Give reduce() an initializer whenever the iterable could plausibly be empty, to avoid a TypeError at runtime.
  • Before reaching for reduce(), check whether a builtin already does the job more clearly: sum(), max(), min(), any(), all(), or math.prod().
  • Remember map()/filter() results are single-use iterators — wrap them in list() or tuple() if you need to iterate more than once or need len()/indexing.
  • When chaining several lazy steps over large or infinite data, prefer generator expressions or itertools functions, which are usually clearer than deeply nested map(...filter(...)) calls.

Practice Exercises

  • Exercise 1: Given words = ["python", "is", "fun", "and", "powerful"], use map() 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], use filter() 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"], use functools.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() and filter() objects are single-use iterators — wrap in list()/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 a lambda; use whichever best communicates intent.
  • Check for a purpose-built builtin (sum(), max(), any(), etc.) before reaching for reduce().