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
# Simulating a very large datasetdef million_numbers(): for i in range(1_000_000): yield i
# ❌ list() — loads ALL 1,000,000 squares into memory at onceeager = list(map(lambda x: x ** 2, million_numbers())) # 1M values sitting in RAM
# ✅ lazy — computes one value at a time, only when neededlazy = map(lambda x: x ** 2, million_numbers())
# next() pulls exactly one value at a time — the remaining 999,995 are never touchedprint(next(lazy)) # 0print(next(lazy)) # 1print(next(lazy)) # 4print(next(lazy)) # 9print(next(lazy)) # 16Each 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.
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 generatedThe 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 used | All N items at once | One item at a time |
| Computation | Everything upfront | Only what is consumed |
| Best when | You need all results | You 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.
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 iteratorHere the Python code:
nums = [1, 2, 3, 4, 5]
# map(func, iterable) — transform every elementsquared = 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 iteratorEach 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 Trueevens = 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 valuetotal = reduce(lambda acc, x: acc + x, nums) # sumproduct = reduce(lambda acc, x: acc * x, nums) # product
print(total) # 15print(product) # 120The accumulation happens step by step:
[1, 2, 3, 4, 5] 1+2 → 3 3+3 → 6 6+4 → 10 10+5 → 15The Trio Together
Section titled “The Trio Together”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 numbersresult = 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)