Loop idioms: range, enumerate, zip
Overview
Python's `for` loop iterates directly over values (`for x in arr:`). But what if you need the index too? Or what if you need to iterate over two arrays at once? Python provides three built-in tools for this: 1. **`range(start, stop, step)`**: Generates a sequence of numbers. - `range(n)` goes from `0` to `n-1`. - `range(len(arr))` is the C-style way to get indices, but it's generally discouraged in Python unless you *only* need the index or are doing index math (like binary search). - Reverse traversal: `range(n - 1, -1, -1)` starts at the last index and goes down to 0. 2. **`enumerate(iterable)`**: Yields pairs of `(index, value)`. This is the Pythonic way to get both the index and the item simultaneously. - `for i, val in enumerate(arr):` unpacks the pair cleanly in the loop header. 3. **`zip(iterA, iterB)`**: Pairs up elements from multiple iterables in lockstep. - `for a, b in zip(list1, list2):` processes the first element of both, then the second, and so on. It stops when the *shortest* iterable is exhausted.
These three functions are the vocabulary of array traversal. `enumerate` prevents messy manual index counters (`i = 0` before the loop). `zip` cleanly handles lockstep traversal without needing to use `range(len(a))` and indexing both arrays. `range` is essential when you need pure index math (like jumping by 2, or searching backward).
Where used: Two-pointer array problems, Iterating over rows and columns, Pairing keys and values
Why learn this
- Knowing `enumerate` saves you from `for i in range(len(arr)): val = arr[i]`.
- Using `zip` prevents `IndexError` when iterating over two lists of different lengths, as it safely stops at the shortest one.
Code walkthrough
names = ['Alice', 'Bob']
scores = [85, 92, 100]
for name, score in zip(names, scores):
print(f"{name} got {score}")
Focus: The loop runs exactly twice. The third score (100) is ignored because `names` only has two elements, and `zip` stops at the shortest list.
Aha moment
arr = ['A', 'B', 'C']
for tup in enumerate(arr):
print(tup)
Prediction: What does `tup` look like on each iteration?
Common guess: It prints A, B, C
`enumerate` actually yields tuples: `(0, 'A')`, `(1, 'B')`, `(2, 'C')`. We normally unpack these immediately by writing `for i, val in enumerate(arr):`.
Common mistakes
- Using range(len(a)) when you also need values: `for i in range(len(arr)): print(arr[i])` works, but `for i, val in enumerate(arr): print(val)` is faster, safer, and idiomatic.
- Off-by-one in reverse ranges: `range(n, 0, -1)` stops at 1. To reach index 0, the stop argument must be `-1`: `range(n-1, -1, -1)`.
- Forgetting that zip stops early: If `a` has 5 items and `b` has 3, `zip(a, b)` will only loop 3 times. The remaining 2 items in `a` are ignored.
Glossary
- unpacking
- Extracting values from a tuple or list into multiple variables at once, e.g., `for idx, val in enumerate(arr):` unpacks the tuple `(idx, val)` yielded by `enumerate`.
Recall questions
- What function should you use if you need both the index and the value while looping over a list?
- What is the stop condition for `zip(a, b)`?
- How do you loop backwards from index 5 down to 0 using `range`?
Understanding checks
What is the last thing printed by this loop?
b
The loop pairs 10 with 'a' and 20 with 'b'. It stops there because `nums` is out of elements. So it prints 10, 'a', 20, 'b'.
What numbers are printed?
3, 2, 1
The `start` is 3, the `step` is -1. The `stop` is 0, which is exclusive, so it stops before hitting 0.
Practice tasks
Refactor to enumerate
The loop below uses the C-style `range(len())` to print the index and value. Refactor it to use `enumerate()` and unpacking.
Continue learning
Previous: for and while loops
Previous: Lists: indexing, slicing, methods
Previous: Tuples: immutability and use cases