map(func, iterable) applies func to each item, returning a lazy iterator in
Python 3 — nothing is computed until you iterate it (or wrap it in list()). This
saves memory on large inputs because results are produced one at a time.
nums = [1, 2, 3]
result = map(lambda x: x * 2, nums)
print(result) # <map object ...> — not evaluated yet
print(list(result)) # [2, 4, 6] — now it runs
Because it's lazy, map never builds the full result in memory unless you ask for
it. In Python 2 map returned a list — a common gotcha when porting code.
Pass several iterables and map calls func with one item from each, in
parallel — func must accept that many arguments. It stops at the shortest
iterable, so mismatched lengths simply truncate.
a = [1, 2, 3]
b = [10, 20, 30]
list(map(lambda x, y: x + y, a, b)) # [11, 22, 33]
list(map(pow, [2, 3, 4], [10, 11])) # [1024, 177147] — stops at 2 items
This is handy for element-wise combining without an explicit zip. If you need the
longest length (padding the gaps), use itertools.zip_longest instead.
filter(predicate, iterable) keeps only the items for which predicate returns
truthy — also a lazy iterator in Python 3. As a special case, passing None as
the predicate keeps the items that are truthy themselves.
nums = [0, 1, 2, 0, 3]
list(filter(lambda x: x > 1, nums)) # [2, 3]
list(filter(None, nums)) # [1, 2, 3] — drops falsy values
Use it to drop elements by a condition. For complex conditions, a comprehension
with if is usually more readable than filter(lambda ...).
functools.reduce(func, iterable, initial) folds an iterable into a single
value by repeatedly applying a two-argument function, carrying an accumulator. In
Python 3 it was moved out of the builtins into functools — Guido considered it
less readable than an explicit loop, so it was demoted to discourage casual use.
from functools import reduce
reduce(lambda acc, x: acc + x, [1, 2, 3, 4], 0) # 10
# step by step: 0+1=1, 1+2=3, 3+3=6, 6+4=10
For common reductions, prefer the dedicated built-ins — sum, max, min,
math.prod, any, all — which are clearer and faster. Reach for reduce only
when no built-in expresses the fold.
A list/generator comprehension is the more Pythonic choice when you'd otherwise
write a lambda, because it's more readable and avoids the function-call overhead
per item. map/filter shine when you can pass an existing named function
(no lambda) and want a lazy iterator with minimal syntax.
nums = [1, 2, 3, 4]
[x * 2 for x in nums if x % 2] # comprehension — clearest with a condition
list(map(str, nums)) # map with a built-in — clean, no lambda
list(map(lambda x: x * 2, nums)) # lambda makes map less readable
Rule of thumb: if it needs a lambda, use a comprehension; if you're mapping an
already-named function, map reads fine. For laziness on huge data, a generator
expression (... for ...) gives both.
map/filter (and generators) are one-shot iterators: iterating them advances
an internal position that is never reset. Once exhausted, they yield nothing on a
second pass — a frequent source of "my second loop is empty" bugs.
doubled = map(lambda x: x * 2, [1, 2, 3])
list(doubled) # [2, 4, 6]
list(doubled) # [] — already consumed!
doubled = list(map(lambda x: x * 2, [1, 2, 3])) # materialize once
sum(doubled); max(doubled) # reuse freely
If you need to iterate more than once, convert to a list (or other concrete collection) up front. Use the lazy iterator directly only when a single pass is enough.
Functionally yes — both are lazy one-pass iterators. map is slightly faster
when f is an existing function (no per-item Python-level call setup), but a
generator expression wins for readability when you'd otherwise need a
lambda. With a lambda, map offers no speed benefit.
map(str, nums) # clean: existing function
(x * 2 for x in nums) # clearer than map(lambda x: x*2, nums)
Rule of thumb: use map with a named function; use a genexpr/comprehension
when the transform is an expression or needs a lambda.
map(f, a, b) pulls one item from each iterable per call and stops at the
shortest — like zip then starmap. The function receives one argument
per iterable.
list(map(lambda x, y: x + y, [1, 2, 3], [10, 20])) # [11, 22] -> stops short
# equivalent to:
from itertools import starmap
list(starmap(lambda x, y: x + y, zip([1,2,3], [10,20])))
Rule of thumb: multi-iterable map is "zip + apply", truncating to the
shortest input.
Passing None as the function makes filter keep only truthy items —
dropping 0, '', None, empty containers, False. It's a quick way to
remove "empty" values.
list(filter(None, [0, 1, "", "a", None, [], [2]])) # [1, 'a', [2]]
Rule of thumb: filter(None, xs) is shorthand for "keep the truthy ones".
A comprehension expresses map + filter together in one readable
expression, avoids lambdas, and returns a concrete list/set/dict directly.
map/filter chained with lambdas get nested and hard to read. Reserve
map/filter for passing existing functions.
[x * 2 for x in nums if x > 0] # clear
list(map(lambda x: x*2, filter(lambda x: x>0, nums))) # noisy
Rule of thumb: comprehension for transform-and-filter logic; map/filter
only when handing off a named function.
They return one-shot iterators, not lists. They do no work until
iterated, can be fully consumed once, and have no len() or
indexing. Forgetting this leads to "empty on the second pass" bugs and
TypeError on len.
m = map(str, range(3))
list(m) # ['0', '1', '2']
list(m) # [] -> already exhausted
len(m) # TypeError
Rule of thumb: wrap in list() if you need to reuse, index, or measure the
result.
Because for most real uses a named built-in or a loop is clearer: sum,
max, min, math.prod, any, all, "".join. reduce forces the reader
to mentally execute a fold, which is error-prone. It now lives in functools
to signal "use sparingly".
from functools import reduce
reduce(lambda a, b: a + b, nums) # prefer sum(nums)
Rule of thumb: reach for a specific built-in; use reduce only for genuinely
custom accumulations.
No. Because map is lazy, map(print, items) does nothing until
consumed, and using it only for side effects is unidiomatic and confusing.
Use a plain for loop when you want effects, not a transformed result.
map(send, messages) # bug: nothing sent (never iterated)
for m in messages: # correct
send(m)
Rule of thumb: map/comprehensions are for producing values; use a for
loop for side effects.
Chain lazy operations — generator expressions or map/filter — so data
flows through one item at a time without building intermediate lists. This
keeps memory flat even for huge or infinite sources.
lines = (l.strip() for l in open("big.log"))
errors = (l for l in lines if "ERROR" in l)
codes = map(parse_code, errors) # nothing materialized yet
first10 = list(islice(codes, 10)) # only now does work happen
Rule of thumb: keep the pipeline lazy and materialize only at the end (or with
islice) to process large data with constant memory.
When your iterable already contains argument tuples and you want them
unpacked into the function. map would pass each tuple as a single
argument; itertools.starmap calls f(*tuple).
from itertools import starmap
points = [(0, 0), (3, 4)]
list(starmap(math.hypot, points)) # [0.0, 5.0]
Rule of thumb: starmap when elements are pre-packed arg tuples; map for
single-argument calls.
Feed (key, value) pairs into dict(). Combine zip to pair keys with
values, or map to compute one side. A dict comprehension is the most
readable for non-trivial logic.
keys, vals = ["a", "b"], [1, 2]
dict(zip(keys, vals)) # {'a': 1, 'b': 2}
dict((k, len(k)) for k in words) # via genexpr
{k: len(k) for k in words} # comprehension, clearest
Rule of thumb: dict(zip(...)) to align two sequences; a dict comprehension
when the value needs computing.
More Functional Programming interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.