Skip to content

map(), filter(), reduce()

In Python, map() and filter() are built-in, while reduce() lives in the functools module. All three return lazy iterators in Python 3, meaning they don’t compute results until needed, which is memory efficient.

Under the hood: realistic scenarios
Need 5 out of a million
# Simulating a very large dataset
def million_numbers():
for i in range(1_000_000):
yield i
# ❌ list() — loads ALL 1,000,000 squares into memory at once
eager = list(map(lambda x: x ** 2, million_numbers())) # 1M values sitting in RAM
# ✅ lazy — computes one value at a time, only when needed
lazy = map(lambda x: x ** 2, million_numbers())
# next() pulls exactly one value at a time — the remaining 999,995 are never touched
print(next(lazy)) # 0
print(next(lazy)) # 1
print(next(lazy)) # 4
print(next(lazy)) # 9
print(next(lazy)) # 16

Each next() call evaluates exactly one item on demand. The iterator simply sits and waits between calls. This is the purest demonstration of lazy evaluation: compute only what you explicitly ask for, nothing more.

Searching a large log file
def read_logs():
for i in range(1_000_000):
print(f" generating line {i}...") # shows exactly what gets evaluated
yield f"LOG {i}: user_action=click, status={'error' if i == 357 else 'ok'}"
logs = read_logs()
errors = filter(lambda log: "error" in log, logs)
first_error = next(errors)
print(first_error)
# Output:
# generating line 0...
# generating line 1...
# generating line 2...
# ... (every line up to 357 is checked by filter)
# generating line 357...
# LOG 357: user_action=click, status=error
# ← stops here, lines 358–999,999 never generated

The only nuance worth noting is that lines 0–356 are evaluated, the filter has to check each one and reject it before reaching 357. So it’s not that only one line is processed, but rather that the pipeline stops as early as possible the moment the condition is met.

The key insight is:

list(map(...))lazy map(...)
Memory usedAll N items at onceOne item at a time
ComputationEverything upfrontOnly what is consumed
Best whenYou need all resultsYou may stop early

Laziness is most valuable when your data is large and you only need part of it: why compute a million squares if you only needed the first five?

map(func, iterable) — Transform Every Element

Section titled “map(func, iterable) — Transform Every Element”

map() takes a function and an iterable, and applies the function to every element in the iterable. It doesn’t modify the original, it returns a new lazy iterator with the transformed values.

Think of it as an assembly line: each item passes through the same operation and comes out the other side changed. Each element passes through the same function, one at a time, independently of the others, exactly like items on an assembly line going through the same machine.

The assembly line
map(lambda x: x ** 2, [1, 2, 3, 4, 5])
Input Function Output
───── ──────── ──────
[ 1 ] ──────────── (x => x ** 2) ──────── [ 1 ]
[ 2 ] ──────────── (x => x ** 2) ──────── [ 4 ]
[ 3 ] ──────────── (x => x ** 2) ──────── [ 9 ]
[ 4 ] ──────────── (x => x ** 2) ──────── [ 16 ]
[ 5 ] ──────────── (x => x ** 2) ──────── [ 25 ]
[1, 2, 3, 4, 5] [1, 4, 9, 16, 25]
original unchanged new iterator

Here the Python code:

nums = [1, 2, 3, 4, 5]
# map(func, iterable) — transform every element
squared = list(map(lambda x: x ** 2, nums))
strings = list(map(str, nums)) # built-in functions work too
print(squared) # [1, 4, 9, 16, 25]
print(strings) # ['1', '2', '3', '4', '5']

filter(func, iterable) — Select Matching Elements

Section titled “filter(func, iterable) — Select Matching Elements”

filter() takes a function and an iterable, and tests every element against the function. Only elements where the function returns True pass through, the rest are discarded.

Unlike map() which transforms every element, filter() makes a yes/no decision on each one, acting like a gatekeeper that only lets certain items through.

Think of it as: “keep only the items that pass this test.”

filter(lambda x: x % 2 == 0, [1, 2, 3, 4, 5, 6])
Input Test (x % 2 == 0) Output
───── ───────────────── ──────
[ 1 ] ───────── False ───────────── ✗ discarded
[ 2 ] ───────── True ───────────── [ 2 ] ──┐
[ 3 ] ───────── False ───────────── ✗ discarded
[ 4 ] ───────── True ───────────── [ 4 ] ──┤
[ 5 ] ───────── False ───────────── ✗ discarded
[ 6 ] ───────── True ───────────── [ 6 ] ──┤
[1, 2, 3, 4, 5, 6] [2, 4, 6]
original unchanged new iterator

Each element faces the same test, it either passes and is kept, or fails and is dropped. The order of the surviving elements is always preserved.

Here the Python code:

nums = [1, 2, 3, 4, 5, 6]
# filter(func, iterable) — keep only elements where func returns True
evens = list(filter(lambda x: x % 2 == 0, nums))
above_3 = list(filter(lambda x: x > 3, nums))
print(evens) # [2, 4, 6]
print(above_3) # [4, 5, 6]

reduce(func, iterable) — Collapse to a Single Value

Section titled “reduce(func, iterable) — Collapse to a Single Value”

reduce() takes a function and an iterable, and folds all elements into a single result by repeatedly applying the function to pairs of values. It carries an accumulator, a running result that gets updated with each element until the list is exhausted.

Unlike map() and filter() which preserve the shape of the collection, reduce() collapses it entirely, like a snowball rolling down a hill, growing with each step until there is nothing left to consume.

Think of it as: “fold all items together into one result.”

reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5])
Step Accumulator Next Element Result
──── ─────────── ──────────── ──────
1 [ 1 ] + [ 2 ] = [ 3 ]
2 [ 3 ] + [ 3 ] = [ 6 ]
3 [ 6 ] + [ 4 ] = [ 10 ]
4 [ 10 ] + [ 5 ] = [ 15 ]
single value
[ 15 ]

The first element always seeds the accumulator, reduce() then walks through the remaining elements one by one, combining each with the running total until only one value remains.

from functools import reduce
nums = [1, 2, 3, 4, 5]
# reduce(func, iterable) — accumulate into a single value
total = reduce(lambda acc, x: acc + x, nums) # sum
product = reduce(lambda acc, x: acc * x, nums) # product
print(total) # 15
print(product) # 120

The accumulation happens step by step:

[1, 2, 3, 4, 5]
1+2 → 3
3+3 → 6
6+4 → 10
10+5 → 15

They compose naturally, you can chain them to build expressive data pipelines:

from functools import reduce
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Sum of squares of even numbers
result = reduce(
lambda acc, x: acc + x,
map(lambda x: x ** 2,
filter(lambda x: x % 2 == 0, nums))
)
print(result) # 220 (4 + 16 + 36 + 64 + 100)