Python map, filter, reduce — Lazy: Practice Problems & Exercises
Practice: map, filter, reduce — Lazy Iteration and the Pipeline Model
← Back to lessonEasy
Use map() with a lambda to produce a new list where each element is doubled.
numbers = [1, 2, 3, 4, 5] result = list(map(lambda x: x * 2, numbers)) print(result)
Solution
numbers = [1, 2, 3, 4, 5]
result = list(map(lambda x: x * 2, numbers))
print(result) # [2, 4, 6, 8, 10]
Explanation: map() returns a lazy map object — no computation happens until iteration. list() forces evaluation. The lambda is applied once per element; Python never allocates an intermediate list.
numbers = [1, 2, 3, 4, 5]
# Use map() to double every element
result = list(map(None, numbers)) # fix the first argument
print(result)Expected Output
[2, 4, 6, 8, 10]Hints
Hint 1: map(func, iterable) applies func to every element lazily.
Hint 2: Pass a lambda that multiplies by 2 as the first argument.
Use filter() to remove all empty strings from tokens. The single-argument filter(None, ...) form works perfectly here.
tokens = ['hello', '', 'world', '', 'python', ''] result = list(filter(None, tokens)) print(result)
Solution
tokens = ['hello', '', 'world', '', 'python', '']
result = list(filter(None, tokens))
print(result) # ['hello', 'world', 'python']
Explanation: When None is passed as the predicate, filter uses the element itself as the truth value. Empty strings are falsy, so they are removed. This works for any falsy value: 0, None, [], {}.
tokens = ['hello', '', 'world', '', 'python', '']
# Use filter() to remove empty strings
result = list(filter(None, tokens))
print(result)Expected Output
['hello', 'world', 'python']Hints
Hint 1: filter(None, iterable) removes all falsy values — empty strings are falsy in Python.
Hint 2: This is the shorthand form; no lambda needed when filtering out falsy values.
Use functools.reduce() with a lambda to compute the sum of numbers without calling sum().
from functools import reduce numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] total = reduce(lambda acc, x: acc + x, numbers) print(total)
Solution
from functools import reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
total = reduce(lambda acc, x: acc + x, numbers)
print(total) # 55
Explanation: reduce starts with the first element as the initial accumulator, then applies the lambda to (acc, next) for each subsequent element. This is the left-fold: ((((1+2)+3)+4)+...+10). For large lists, sum() is faster because it is implemented in C, but reduce generalises to any binary operation.
from functools import reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Use reduce() to sum the list (no built-in sum())
total = reduce(None, numbers) # fix the first argument
print(total)Expected Output
55Hints
Hint 1: reduce(func, iterable) folds the list left-to-right into a single value.
Hint 2: The accumulator function receives (accumulated_value, next_element).
Hint 3: Use a lambda that adds two numbers.
Use map() with a two-argument lambda to produce the element-wise product of xs and ys.
xs = [1, 2, 3, 4] ys = [10, 20, 30, 40] result = list(map(lambda x, y: x * y, xs, ys)) print(result)
Solution
xs = [1, 2, 3, 4]
ys = [10, 20, 30, 40]
result = list(map(lambda x, y: x * y, xs, ys))
print(result) # [10, 40, 90, 160]
Explanation: map() accepts multiple iterables and zips them together, passing one element from each to the function simultaneously. It stops at the shortest iterable (like zip()).
xs = [1, 2, 3, 4]
ys = [10, 20, 30, 40]
# Use map() with two iterables to compute element-wise products
result = list(map(None, xs, ys)) # fix the call
print(result)Expected Output
[10, 40, 90, 160]Hints
Hint 1: map(func, iter1, iter2) passes pairs of elements to func.
Hint 2: The lambda should accept two arguments: lambda x, y: ...
Medium
Filter out negative numbers, then square the positives. Build the pipeline using only filter() and map(), wrapping in list() only at the end.
data = [1, -2, 3, -4, 5, -6, 7, -8, 9, -10] result = list(map(lambda x: x * x, filter(lambda x: x > 0, data))) print(result)
Solution
data = [1, -2, 3, -4, 5, -6, 7, -8, 9, -10]
result = list(map(lambda x: x * x, filter(lambda x: x > 0, data)))
print(result) # [1, 9, 25, 49, 81]
Explanation: filter(...) produces a lazy iterator of positives. map(...) wraps it — still lazy. list() pulls from map, which pulls from filter, which pulls from data. The chain executes in a single pass with no intermediate list. This is Python's lazy pipeline model.
data = [1, -2, 3, -4, 5, -6, 7, -8, 9, -10]
# Step 1: filter out negative numbers
# Step 2: square the remaining numbers
# Use only map() and filter() — no list comprehensions
result = list(None) # build the pipeline here
print(result)Expected Output
[1, 9, 25, 49, 81]Hints
Hint 1: Build left to right: filter first, then map.
Hint 2: Neither map nor filter materialises a list — chain them lazily.
Hint 3: Wrap only the outermost call in list() to force evaluation.
Use reduce() to flatten nested (a list of lists) into a single flat list using list concatenation.
from functools import reduce nested = [[1, 2], [3, 4], [5, 6], [7, 8]] flat = reduce(lambda acc, lst: acc + lst, nested) print(flat)
Solution
from functools import reduce
nested = [[1, 2], [3, 4], [5, 6], [7, 8]]
flat = reduce(lambda acc, lst: acc + lst, nested)
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8]
Explanation: Each acc + lst creates a new list (O(n) copy). For very large inputs, prefer itertools.chain.from_iterable() which is O(1) per step. The reduce approach is clean for moderate sizes and clearly demonstrates the left-fold pattern. An explicit initial value reduce(..., nested, []) is safer when nested might be empty.
from functools import reduce
nested = [[1, 2], [3, 4], [5, 6], [7, 8]]
# Use reduce() to flatten into a single list
flat = reduce(None, nested) # fix
print(flat)Expected Output
[1, 2, 3, 4, 5, 6, 7, 8]Hints
Hint 1: The accumulator starts as the first sublist.
Hint 2: Each step should concatenate the current accumulator with the next sublist.
Hint 3: List concatenation operator is +.
Use reduce() with an empty dict as the initial accumulator to count word frequencies.
from functools import reduce
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
freq = reduce(lambda acc, w: {**acc, w: acc.get(w, 0) + 1}, words, {})
print(freq)Solution
from functools import reduce
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
freq = reduce(lambda acc, w: {**acc, w: acc.get(w, 0) + 1}, words, {})
print(freq) # {'apple': 3, 'banana': 2, 'cherry': 1}
Explanation: {**acc, w: acc.get(w, 0) + 1} creates a new dict on every step by spreading the existing accumulator and overwriting the key w with its incremented count. This is pure (no mutation), but O(n) space per step. A mutable alternative: use a dict.setdefault trick or just use collections.Counter in production code.
from functools import reduce
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
# Use reduce() to build a frequency dict — no Counter, no defaultdict
freq = reduce(None, words) # fix
print(freq)Expected Output
{'apple': 3, 'banana': 2, 'cherry': 1}Hints
Hint 1: The accumulator should be a dict, so provide an empty dict as the initial value.
Hint 2: Inside the lambda, you cannot use assignment — use dict.get() to read the current count.
Hint 3: Trick: you can mutate the accumulator dict inside the lambda using dict.__setitem__ or a helper.
Hint 4: Pattern: lambda acc, w: {**acc, w: acc.get(w, 0) + 1}
Use map() with operator.add (no lambda) and the two-iterable form of map to compute the sum of each (a, b) pair.
import operator pairs = [(1, 2), (3, 4), (5, 6), (7, 8)] a_vals, b_vals = zip(*pairs) sums = list(map(operator.add, a_vals, b_vals)) print(sums)
Solution
import operator
pairs = [(1, 2), (3, 4), (5, 6), (7, 8)]
a_vals, b_vals = zip(*pairs)
sums = list(map(operator.add, a_vals, b_vals))
print(sums) # [3, 7, 11, 15]
Explanation: operator.add is a C-level callable — faster than a Python lambda for large datasets because it skips the lambda call overhead. zip(*pairs) transposes the list of pairs into two tuples of first and second elements. The two-iterable map form then applies operator.add element-wise.
import operator
pairs = [(1, 2), (3, 4), (5, 6), (7, 8)]
# Use map() with operator.add (NOT a lambda) to sum each tuple
sums = list(map(None, *zip(*pairs))) # fix — use map with operator.add
print(sums)Expected Output
[3, 7, 11, 15]Hints
Hint 1: operator.add(a, b) is a regular callable equivalent to a + b.
Hint 2: map(operator.add, iter1, iter2) works like the two-iterable map from problem 4.
Hint 3: Unpack pairs into two iterables using zip(*pairs).
Hard
Use reduce() to build a list of running maximums. At each position i, the value should be the maximum of all elements up to and including index i.
from functools import reduce
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
running_max = reduce(
lambda acc, x: acc + [max(acc[-1], x)] if acc else [x],
numbers,
[]
)
print(running_max)Solution
from functools import reduce
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
running_max = reduce(
lambda acc, x: acc + [max(acc[-1], x)] if acc else [x],
numbers,
[]
)
print(running_max)
# [3, 3, 4, 4, 5, 9, 9, 9, 9, 9, 9]
Explanation: The accumulator acc is a growing list. On each step, acc[-1] is the running maximum so far; we extend with max(acc[-1], x). The ternary handles the empty-acc edge case (first element). Note that acc + [...] creates a new list on every step — for production code, use itertools.accumulate(numbers, max) which is O(n) total.
from functools import reduce
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
# Use reduce() to produce a list of running maximums.
# running_max[i] = max(numbers[0], ..., numbers[i])
# Expected: [3, 3, 4, 4, 5, 9, 9, 9, 9, 9, 9]
running_max = reduce(None, numbers) # fix
print(running_max)Expected Output
[3, 3, 4, 4, 5, 9, 9, 9, 9, 9, 9]Hints
Hint 1: The accumulator should be a list of running maximums seen so far.
Hint 2: Provide an initial value of [] for the accumulator.
Hint 3: Each step: append max(acc[-1], current) to acc — but lambdas cannot use append.
Hint 4: Use list concatenation: acc + [max(acc[-1], x)] if acc else [x]
Implement prime_squares(limit, n) using a lazy filter + map pipeline. Use itertools.islice to take only the first n results.
import itertools
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def prime_squares(limit, n):
primes = filter(is_prime, range(2, limit + 1))
pairs = map(lambda p: (p, p * p), primes)
return list(itertools.islice(pairs, n))
print(prime_squares(100, 5))Solution
import itertools
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def prime_squares(limit, n):
primes = filter(is_prime, range(2, limit + 1))
pairs = map(lambda p: (p, p * p), primes)
return list(itertools.islice(pairs, n))
print(prime_squares(100, 5))
# [(2, 4), (3, 9), (5, 25), (7, 49), (11, 121)]
Explanation: Every step is lazy: range, filter, map, and islice are all iterators. list() at the end pulls exactly n elements, stopping early. No intermediate list of 99 integers is ever created. This is the pipeline model: data flows element by element from source to sink in a single pass.
# Build a lazy pipeline that:
# 1. Generates integers from 2 up to limit (inclusive)
# 2. Filters to keep only primes (using filter + a primality lambda)
# 3. Maps each prime p to (p, p*p)
# 4. Returns the first n results as a list
#
# Do NOT materialise the integer range early — keep it lazy.
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def prime_squares(limit, n):
pass # implement using filter() and map() lazily
print(prime_squares(100, 5))
# Expected: [(2, 4), (3, 9), (5, 25), (7, 49), (11, 121)]Expected Output
[(2, 4), (3, 9), (5, 25), (7, 49), (11, 121)]Hints
Hint 1: range(2, limit + 1) is already lazy — do not convert it to a list.
Hint 2: filter(is_prime, range(...)) stays lazy.
Hint 3: map(lambda p: (p, p*p), filtered) also stays lazy.
Hint 4: Use itertools.islice to take only the first n without exhausting the iterator.
Implement compose_many(*funcs) that returns a single callable applying all functions right-to-left. Use reduce() internally.
from functools import reduce
def compose_many(*funcs):
return reduce(lambda f, g: lambda x: f(g(x)), funcs)
steps = [
str.strip,
str.upper,
lambda s: s + '!',
lambda s: s * 2,
]
transform = compose_many(*steps)
print(transform(' hello ')) # HELLO!HELLO!Solution
from functools import reduce
def compose_many(*funcs):
return reduce(lambda f, g: lambda x: f(g(x)), funcs)
steps = [
str.strip,
str.upper,
lambda s: s + '!',
lambda s: s * 2,
]
transform = compose_many(*steps)
print(transform(' hello ')) # HELLO!HELLO!
Explanation: reduce(lambda f, g: lambda x: f(g(x)), funcs) folds the list of functions into a single composed callable. For [f, g, h], the fold produces lambda x: f(g_h(x)) where g_h = lambda x: g(h(x)) — i.e., right-to-left application. This is the standard functional definition of function composition. Because each reduce step creates a new lambda that closes over f and g, the whole chain is O(1) to build and executes left-to-right through the closures.
from functools import reduce
def compose_many(*funcs):
# Return a single function that applies funcs right-to-left.
# compose_many(f, g, h)(x) => f(g(h(x)))
pass
# Test pipeline: strip whitespace, uppercase, add exclamation, repeat twice
steps = [
str.strip,
str.upper,
lambda s: s + '!',
lambda s: s * 2,
]
transform = compose_many(*steps)
print(transform(' hello ')) # HELLO!HELLO!Expected Output
HELLO!HELLO!Hints
Hint 1: compose_many should reduce the list of functions right-to-left.
Hint 2: Mathematical composition: (f o g)(x) = f(g(x)) — g runs first.
Hint 3: reduce(lambda f, g: lambda x: f(g(x)), funcs) builds the chain.
Hint 4: Right-to-left means the last function in the list runs first on the input.
