16.6 enumerate() and zip(): Idiomatic Iteration
While simple for loops that iterate over a sequence are foundational, Python provides two built-in functions, enumerate() and zip(), that elevate iteration from merely processing items to intelligently managing their context and relationships. These functions are cornerstones of idiomatic Python, allowing for cleaner, more expressive, and less error-prone code.
The Power of enumerate(): Accessing Index and Value
Often, within a loop, you need access to both the current item and its positional index. The novice approach might involve initializing a counter variable before the loop and manually incrementing it inside.
# The non-idiomatic way
fruits = ['apple', 'banana', 'cherry']
index = 0
for fruit in fruits:
print(f"Index {index}: {fruit}")
index += 1
This method is verbose and prone to error, such as forgetting to increment the counter. The enumerate() function elegantly solves this by returning an iterator that yields pairs containing a count (which starts at 0 by default) and the values obtained from iterating over the given sequence.
# The idiomatic way using enumerate()
fruits = ['apple', 'banana', 'cherry']
for index, fruit in enumerate(fruits):
print(f"Index {index}: {fruit}")
The true power of enumerate() lies in its optional start parameter. You are not locked into zero-based indexing; you can start the count from any integer. This is particularly useful when you need to present human-friendly numbers, such as in a numbered list for user selection.
# Starting the enumeration from 1
tasks = ['Write report', 'Submit invoice', 'Schedule meeting']
for task_number, task_name in enumerate(tasks, start=1):
print(f"{task_number}. {task_name}")
# Output:
# 1. Write report
# 2. Submit invoice
# 3. Schedule meeting
Under the hood, enumerate(iterable, start=0) returns an iterator that, for each step, yields a tuple (index, element). The for loop seamlessly unpacks this tuple into the variables you provide (index and fruit in the example above).
Combining Sequences with zip()
Where enumerate() adds a dimension (the index) to a single iterable, zip() is used to aggregate elements from two or more iterables. It returns an iterator of tuples where the i-th tuple contains the i-th element from each of the argument sequences or iterables. The loop stops at the length of the shortest iterable.
A common use case is processing corresponding elements from parallel lists.
names = ['Alice', 'Bob', 'Charlie']
scores = [95, 87, 91]
# Combining names and scores
for name, score in zip(names, scores):
print(f"{name} scored {score} points.")
# Output:
# Alice scored 95 points.
# Bob scored 87 points.
# Charlie scored 91 points.
This is vastly superior and more readable than the alternative of iterating over indices with range(len(names)) and using those indices to access both lists.
Handling Mismatched Lengths with zip()
A critical behavior and common pitfall of zip() is that it silently truncates to the shortest iterable. This can be a source of subtle bugs if you assume it will iterate over the length of the longest list.
list_a = [1, 2, 3, 4, 5]
list_b = ['a', 'b', 'c'] # Shorter list
for num, letter in zip(list_a, list_b):
print(num, letter)
# Output:
# 1 a
# 2 b
# 3 c
# The elements 4 and 5 from list_a are ignored.
If you need to iterate until the longest iterable is exhausted, filling in missing values with a placeholder, you should use itertools.zip_longest() from the standard library’s itertools module.
from itertools import zip_longest
list_a = [1, 2, 3, 4, 5]
list_b = ['a', 'b', 'c']
for num, letter in zip_longest(list_a, list_b, fillvalue='N/A'):
print(num, letter)
# Output:
# 1 a
# 2 b
# 3 c
# 4 N/A
# 5 N/A
Combining enumerate() and zip()
For maximum control and information, enumerate() and zip() can be powerfully combined. This pattern gives you the index and the corresponding elements from multiple sequences simultaneously.
headers = ['Name', 'Score', 'Grade']
data = [('Alice', 95, 'A'), ('Bob', 87, 'B+'), ('Charlie', 91, 'A-')]
# Print a table with headers, using the index to format
for i, (name, score, grade) in enumerate(zip(headers, *data)):
print(f"{i}: {name:<10} {score:<6} {grade}")
# Output:
# 0: Name Alice A
# 1: Score 95 A-
# 2: Grade Bob B+
In this advanced example, zip(headers, *data) first transposes the data list of tuples into columns, and enumerate() then numbers each row of this transposed table. This demonstrates how these tools can be composed to handle complex data transformation tasks succinctly within a loop.