Python enumerate, zip, map and filter
Learn how Python's enumerate, zip, map, and filter built-ins work, with clear examples and practical patterns for cleaner iteration code.
Python ships four built-in functions — enumerate, zip, map, and filter — that replace common loop patterns with a single, readable expression. Each one returns an iterator (lazy, memory-efficient), and they compose naturally with for loops and list comprehensions.
This chapter covers:
enumerate()— loop with an automatic indexzip()— iterate over two or more sequences in parallelmap()— apply a function to every itemfilter()— keep only items that pass a test- Combining all four in real-world patterns
- When to prefer a list comprehension instead
enumerate() — index while looping
When you need both the position and the value during a loop, the naive approach uses a manual counter:
colors = ["red", "green", "blue"]
i = 0
for color in colors:
print(i, color)
i += 1enumerate() does this automatically. It wraps any iterable and yields (index, value) pairs:
colors = ["red", "green", "blue"]
for i, color in enumerate(colors):
print(i, color)Output:
0 red
1 green
2 blueStarting from a different index
Pass a start argument to begin counting from any integer:
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")Output:
1. apple
2. banana
3. cherryThis is useful when building numbered lists for display, where 0-based numbering looks odd to the reader.
enumerate() with a condition
Because enumerate() is just a regular iterator, you can add if tests inside the loop body:
scores = [72, 91, 55, 88, 44]
for rank, score in enumerate(scores, start=1):
if score >= 80:
print(f"Rank {rank}: score {score} — PASS")Output:
Rank 2: score 91 — PASS
Rank 4: score 88 — PASSGetting just the index without enumerate()
If you only need an index and nothing else, range(len(seq)) works — but as soon as you also need the value, reach for enumerate() to avoid writing seq[i] repeatedly.
zip() — iterate over several sequences in parallel
zip() combines two or more iterables into a single iterator of tuples, pairing items at matching positions:
names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")Output:
Alice: 95
Bob: 87
Charlie: 92zip() with three or more iterables
first = ["a", "b", "c"]
second = [1, 2, 3]
third = [True, False, True]
for x, y, z in zip(first, second, third):
print(x, y, z)Output:
a 1 True
b 2 False
c 3 TrueUnequal-length sequences
zip() stops at the shortest iterable and silently drops the remaining items from longer ones:
short = [1, 2]
long = [10, 20, 30, 40]
print(list(zip(short, long)))Output:
[(1, 10), (2, 20)]The values 30 and 40 from long are discarded. If you need to keep all items, use itertools.zip_longest from the standard library:
from itertools import zip_longest
short = [1, 2]
long = [10, 20, 30, 40]
print(list(zip_longest(short, long, fillvalue=0)))Output:
[(1, 10), (2, 20), (0, 30), (0, 40)]Converting a zipped result to a list or dict
zip() returns a lazy iterator. Wrap it in list() to materialize all pairs at once, or pass it to dict() to build a mapping from two parallel sequences:
keys = ["name", "age", "city"]
values = ["Alice", 30, "Paris"]
record = dict(zip(keys, values))
print(record)Output:
{'name': 'Alice', 'age': 30, 'city': 'Paris'}Unzipping with zip(*...)
The * unpacking trick "reverses" a sequence of tuples back into separate sequences:
pairs = [(1, "a"), (2, "b"), (3, "c")]
numbers, letters = zip(*pairs)
print(numbers) # (1, 2, 3)
print(letters) # ('a', 'b', 'c')map() — apply a function to every item
map(function, iterable) applies function to every item and returns a lazy iterator of results. It replaces a for loop that builds a new list by transforming each element.
numbers = [1, 2, 3, 4, 5]
squares = map(lambda x: x ** 2, numbers)
print(list(squares))Output:
[1, 4, 9, 16, 25]map() with a named function
You can pass any callable — a built-in, a function defined with def, or a lambda:
words = [" hello ", " world ", " python "]
stripped = list(map(str.strip, words))
print(stripped)Output:
['hello', 'world', 'python']Here str.strip is passed as a reference (no parentheses). map() calls str.strip(word) for each item.
map() over multiple iterables
Pass additional iterables to map a function that takes two or more arguments. map() then steps through all iterables in parallel (like zip(), it stops at the shortest):
a = [1, 2, 3]
b = [10, 20, 30]
totals = list(map(lambda x, y: x + y, a, b))
print(totals)Output:
[11, 22, 33]map() vs list comprehension
Both approaches are valid. The rule of thumb:
| Situation | Prefer |
|---|---|
| Applying an existing named function | map() — shorter, no lambda needed |
| Writing an inline expression | List comprehension — more readable |
| Multiple inputs in parallel | map() with multiple iterables |
Equivalent forms:
# map() with a named function
result = list(map(str.upper, ["a", "b", "c"]))
# list comprehension — same result
result = [s.upper() for s in ["a", "b", "c"]]filter() — keep only items that match a condition
filter(function, iterable) keeps items for which function returns a truthy value. It returns a lazy iterator.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = filter(lambda x: x % 2 == 0, numbers)
print(list(evens))Output:
[2, 4, 6, 8, 10]filter() with a named function
def is_positive(n):
return n > 0
values = [-3, -1, 0, 4, 7, -2]
positives = list(filter(is_positive, values))
print(positives)Output:
[4, 7]filter(None, iterable) — remove falsy values
Passing None as the function keeps only truthy items. This is a quick way to drop empty strings, zeros, and None values:
mixed = [0, "hello", "", None, 42, False, "world"]
truthy = list(filter(None, mixed))
print(truthy)Output:
['hello', 42, 'world']filter() vs list comprehension
filter() and a conditional list comprehension do the same job:
numbers = range(1, 11)
# filter()
evens_f = list(filter(lambda x: x % 2 == 0, numbers))
# list comprehension — equivalent
evens_c = [x for x in numbers if x % 2 == 0]
print(evens_f) # [2, 4, 6, 8, 10]
print(evens_c) # [2, 4, 6, 8, 10]Prefer a list comprehension when the condition is written inline; prefer filter() when you already have a well-named predicate function.
Combining enumerate, zip, map, and filter
These four functions compose naturally. Here is a realistic example: you have two parallel lists of student names and raw scores; you want to print a numbered report that only includes students who passed (score >= 60), with scores converted to letter grades.
def letter_grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
return "F"
names = ["Alice", "Bob", "Carol", "Dave", "Eve"]
scores = [85, 52, 91, 67, 48]
# Step 1: pair names with scores
pairs = zip(names, scores)
# Step 2: keep only passing students
passing = filter(lambda pair: pair[1] >= 60, pairs)
# Step 3: convert scores to letter grades
graded = map(lambda pair: (pair[0], letter_grade(pair[1])), passing)
# Step 4: number the results
for rank, (name, grade) in enumerate(graded, start=1):
print(f"{rank}. {name}: {grade}")Output:
1. Alice: B
2. Carol: A
3. Dave: DEach step produces an iterator; nothing is materialised into a list until the for loop consumes it. This keeps memory use flat regardless of input size.
Common gotchas
Iterators are exhausted after one pass
map(), filter(), and zip() all return iterators, not lists. Once you consume an iterator, it is empty:
squares = map(lambda x: x ** 2, [1, 2, 3])
print(list(squares)) # [1, 4, 9]
print(list(squares)) # [] — already exhaustedWrap in list() up front if you need to iterate more than once.
zip() silently drops trailing items
When the input sequences have different lengths, zip() discards the extra elements without warning. Always check lengths explicitly if this matters, or use itertools.zip_longest.
enumerate() does not change the iterable
enumerate() wraps the original iterable; it does not copy it. Modifying the underlying list during iteration can produce unexpected results — the same caution that applies to ordinary for loops.
Quick reference
| Function | Signature | What it returns |
|---|---|---|
enumerate | enumerate(iterable, start=0) | Iterator of (index, value) tuples |
zip | zip(*iterables) | Iterator of tuples, one item from each iterable per tuple |
map | map(function, *iterables) | Iterator of function applied to each item |
filter | filter(function, iterable) | Iterator of items where function returns True |
All four are lazy: they produce values on demand rather than building a full list up front.