Learning Goals
3 minBy the end of this lesson you can:
- Use
map(func, iterable)to transform every item. - Use
filter(pred, iterable)to keep items that pass a test. - Realise
mapandfilterreturn iterators — wrap inlist()to materialise. - Pick between comprehensions and map/filter based on readability.
Warm-Up · Three Ways to Square
5 minnums = [1, 2, 3, 4, 5] # 1 — explicit loop out = [] for n in nums: out.append(n * n) # 2 — comprehension out = [n * n for n in nums] # 3 — map out = list(map(lambda n: n * n, nums))
Three roads, same destination. Today we'll learn when each is best.
map and filter are functional-programming classics from the 70s. Python supports them, but in 99% of cases a comprehension is clearer. Knowing them matters for two reasons: (1) you'll read code that uses them, (2) they pair well with already-defined named functions.
New Concept · Two Built-ins
14 minmap(func, iterable)
Applies func to each item, yields the results.
numbers = [1, 2, 3, 4] # With a lambda list(map(lambda n: n * 2, numbers)) # [2, 4, 6, 8] # With a named function — this is where map shines def square(n): return n * n list(map(square, numbers)) # [1, 4, 9, 16] # String methods work too words = ["hi", "there"] list(map(str.upper, words)) # ['HI', 'THERE']
Notice list(map(...)). map returns an iterator, not a list. It's lazy — items are computed only when asked for. We'll dive into iterators in PY-L3-26.
map with multiple iterables
map(func, iter1, iter2) walks both iterables in lockstep, passing one item from each to func.
a = [1, 2, 3] b = [10, 20, 30] list(map(lambda x, y: x + y, a, b)) # [11, 22, 33] # Like zip + comprehension: [x + y for x, y in zip(a, b)] # [11, 22, 33]
filter(pred, iterable)
Keeps items where pred(item) is truthy. Drops the rest.
numbers = [1, -2, 3, -4, 5, 0, -6] list(filter(lambda n: n > 0, numbers)) # [1, 3, 5] # With a named predicate def is_even(n): return n % 2 == 0 list(filter(is_even, numbers)) # [-2, -4, 0, -6] # None as predicate keeps truthy items data = [0, 1, "", "hi", None, "world"] list(filter(None, data)) # [1, 'hi', 'world']
The filter(None, seq) trick is a quick way to drop falsy items — zero, empty string, None, empty containers.
The big comparison
Task: double every positive number
Loop: out = []
for n in numbers:
if n > 0:
out.append(n * 2)
map+filter: list(map(lambda n: n * 2,
filter(lambda n: n > 0, numbers)))
Comprehension:[n * 2 for n in numbers if n > 0] ✅ clearestThe comprehension wins on readability. Once you can mix transform + filter in one comp, map + filter is just nostalgia.
When map/filter still wins
One specific case: a single named function with no other change.
# A named function — passing it directly to map is clean def normalise(name): return name.strip().lower() clean = list(map(normalise, raw_names)) # The comp version repeats the function name clean = [normalise(name) for name in raw_names]
Slightly shorter with map. Many Python folks still prefer the comp because the loop variable makes the intent clearer. Both are fine.
Don't forget list()
Without list() the result is an iterator. Print it and you get something like <map object at 0x...>. Iterate it once and it's exhausted — running it a second time gives nothing. We'll explore why in PY-L3-26.
result = map(lambda n: n * 2, [1, 2, 3]) print(list(result)) # [2, 4, 6] print(list(result)) # [] -- iterator already exhausted
Worked Example · The Pricing Pipeline
12 minSave as pricing.py:
# pricing.py — map and filter applied to messy data raw = [ " RM 12.50 ", "RM 8.00", "free", # bad entry " ", # blank "RM 23.75", "RM 5.50", "RM -2.00", # bad — negative ] # Strategy # 1) clean each entry # 2) keep only ones starting with "RM " # 3) parse the number # 4) keep only positive prices def clean(s): return s.strip() def is_price(s): return s.startswith("RM ") def to_amount(s): return float(s.split()[1]) def is_positive(n): return n > 0 # Pipeline with map and filter cleaned = map(clean, raw) prices = filter(is_price, cleaned) amounts = map(to_amount, prices) positive = filter(is_positive, amounts) result = list(positive) print(result) # → [12.5, 8.0, 23.75, 5.5] print(f"Total: RM {sum(result):.2f}") # Same thing with comprehensions for comparison again = [float(s.strip().split()[1]) for s in raw if s.strip().startswith("RM ") and float(s.strip().split()[1]) > 0] print(again) # same result
Read the diff
The map/filter pipeline reads as a chain of clearly-named stages. The comprehension version is more compact but repeats .strip().split()[1] — once for the filter and once for the value. Sometimes the pipeline style wins when each stage deserves its own name; sometimes the comp wins when the operations are simple enough to inline.
Map and filter shine when you have small named functions (clean, is_price, to_amount) and you want to compose them. That's exactly the use case in data-processing libraries and ETL pipelines.
Try It Yourself
13 minUse map to double every element of [1, 2, 3, 4, 5]. Print as a list.
Hint
print(list(map(lambda n: n * 2, [1, 2, 3, 4, 5]))) # → [2, 4, 6, 8, 10]
Use filter(None, ...) on [0, "hi", "", [1], [], None, "ok"]. Print the survivors.
Hint
data = [0, "hi", "", [1], [], None, "ok"] print(list(filter(None, data))) # → ['hi', [1], 'ok']
filter(None, ...) is the classic "drop falsy values" idiom.
Given raw = ["3", " 5 ", "abc", "7", "", "-2"], use map + filter to: strip whitespace, drop blanks, convert to int, drop non-positive numbers, then sum the result.
Hint
raw = ["3", " 5 ", "abc", "7", "", "-2"] def safe_int(s): try: return int(s) except ValueError: return None stripped = map(str.strip, raw) # ["3", "5", "abc", "7", "", "-2"] non_blank = filter(None, stripped) # drop empties as_int = map(safe_int, non_blank) # ["abc" → None] valid = filter(lambda x: x is not None, as_int) positive = filter(lambda x: x > 0, valid) print(sum(positive)) # 15 = 3 + 5 + 7
Each stage is one transformation. Compose with map and filter. The comp version would need a try/except inside a comprehension — clumsier.
Mini-Challenge · Both Styles, Same Result
8 minImplement four tasks twice — once with map/filter, once with a comprehension. The outputs must match.
- From
[1..20], get squares of just the multiples of 3. - From
["Hi", "", "world", " "], get uppercase of just the non-blank strings. - From
[3.7, 2.1, 8.9, 5.5], get only the values rounded to integers that are even. - From
[(1, 2), (3, 4), (5, 6)], get the sum of each pair.
Show one possible solution
# 1 mf = list(map(lambda n: n * n, filter(lambda n: n % 3 == 0, range(1, 21)))) cp = [n * n for n in range(1, 21) if n % 3 == 0] assert mf == cp # 2 mf = list(map(str.upper, filter(str.strip, ["Hi", "", "world", " "]))) cp = [s.upper() for s in ["Hi", "", "world", " "] if s.strip()] assert mf == cp # 3 mf = list(filter(lambda n: n % 2 == 0, map(round, [3.7, 2.1, 8.9, 5.5]))) cp = [round(x) for x in [3.7, 2.1, 8.9, 5.5] if round(x) % 2 == 0] assert mf == cp # 4 pairs = [(1, 2), (3, 4), (5, 6)] mf = list(map(sum, pairs)) cp = [sum(p) for p in pairs] assert mf == cp print("All four pairs match.")
Non-negotiables: every task implemented twice, assertions confirming equivalence. Notice #2 uses str.strip as a predicate — an empty string is falsy, so filter drops it.
Recap
3 minmap(func, seq) applies a function to each item. filter(pred, seq) keeps items where pred is truthy. Both return iterators — wrap in list() to see results. Comprehensions usually win on readability; map/filter shine when you have well-named functions to compose. filter(None, seq) is the "drop falsy values" idiom worth memorising.
Vocabulary Card
- map(func, seq)
- Lazy iterator yielding
func(x)for eachxinseq. - filter(pred, seq)
- Lazy iterator yielding items where
pred(x)is truthy. - iterator
- A lazy stream of values. Computed on demand. Exhausted after one pass. Tomorrow's topic.
- filter(None, seq)
- Drop all falsy values — 0, "", [], None.
Homework
4 minBuild names_pipeline.py. Given:
raw = [" Aisyah", "", "WEI JIE ", "priya", None, "iman", " AIZAT ", ""]
Build a pipeline that:
- Drops
Noneand empty/whitespace-only strings. - Strips whitespace.
- Title-cases each name.
- Keeps only names of at least 5 letters.
Write it twice — once with map/filter, once with a comprehension. Print both; assert they're equal.
Sample · names_pipeline.py
raw = [" Aisyah", "", "WEI JIE ", "priya", None, "iman", " AIZAT ", ""] def not_blank(s): return s and s.strip() # Pipeline non_none = filter(None, raw) non_blank2 = filter(str.strip, non_none) stripped = map(str.strip, non_blank2) titled = map(str.title, stripped) long_only = filter(lambda n: len(n) >= 5, titled) mf_result = list(long_only) # Comprehension cp_result = [s.strip().title() for s in raw if s and s.strip() and len(s.strip()) >= 5] print("map/filter:", mf_result) print("comp :", cp_result) assert mf_result == cp_result, "Mismatch!" print("Both pipelines agree.")
Non-negotiables: filter(None, raw) drops the None first, then chained map/filter calls. The comp version has three checks combined with and. Both produce the same list; the assert proves it.