Looping like a Pythonista: enumerate, zip & items
Loop with a counter (enumerate), over two lists at once (zip), and over a dict (.items()).
Loop over what you have, not over indexes
Reaching for range(len(items)) and indexing back with items[i] is the beginner tell. It reads noisily, breaks the moment you rename or reorder things, and is the classic home of off-by-one bugs. Python hands you iterators that give you exactly what you need, so you loop over the data itself instead of bookkeeping positions.
| Instead of | Write | Each turn gives you |
|---|---|---|
| for i in range(len(xs)): xs[i] | for x in xs | the value |
| for i in range(len(xs)): i, xs[i] | for i, x in enumerate(xs) | the position and the value |
| for i in range(len(a)): a[i], b[i] | for x, y in zip(a, b) | one item from each, in lockstep |
| for k in d: k, d[k] | for k, v in d.items() | the key and its value |
enumerate: the value plus its position
enumerate(iterable) wraps any iterable and yields (index, value) pairs, lazily, one at a time.
for i, letter in enumerate(["a", "b", "c"]):
print(i, letter)
# 0 a
# 1 b
# 2 c
Counting starts at 0. Need 1-based numbering (line numbers, ranks)? Pass start: enumerate(items, start=1). Do not hand-roll i + 1, and do not fall back to range. Each pair is a tuple, so i, letter unpacks it. When you actually need a [index, value] list (the Apply asks for exactly this), build one per item: [i, value].
zip: walk several sequences in lockstep
zip(a, b) pairs items by position: first of a with first of b, second with second, and so on. It is how you iterate two parallel lists without indexing either.
for name, score in zip(["Ada", "Sam"], [90, 85]):
print(name, score)
# Ada 90
# Sam 85
To collect [name, score] lists (the Practice), build [name, score] inside the loop or a comprehension.
.items(): keys and values from a dict together
Iterating a dict directly gives only keys. .items() gives both:
prices = {"apple": 3, "pear": 2}
for fruit, price in prices.items():
print(fruit, price)
# apple 3
# pear 2
.keys() and .values() give one side each. Since Python 3.7, all three iterate in insertion order.
Pitfalls
zipsilently truncates to the shortest input.zip(["a", "b", "c"], [1, 2])yields only two pairs and drops"c"with no error. If your lists are meant to be the same length, that hides a data bug. Fix: assertlen(a) == len(b)first, or usezip(a, b, strict=True)(Python 3.10+), which raisesValueErroron a length mismatch.
enumerateyields tuples, not lists.list(enumerate(["a"]))is[(0, "a")]. If the caller expects[0, "a"], convert explicitly.
Interview nuance: both enumerate and zip return lazy iterators in Python 3, not lists. They pull one item at a time and use O(1) extra memory regardless of input size, which is why they scale to large or streaming data. The catch is single-pass: an iterator is exhausted after one loop. z = zip(a, b); list(z) gives the pairs, but a second list(z) gives [], because the first pass consumed it. Wrap in list(...) once if you need to iterate the result more than once.
for i, letter in enumerate(["a", "b", "c"]):
print(i, letter)
for name, score in zip(["Ada", "Sam"], [90, 85]):
print(name, score)Apply
Your turn
The task this lesson builds to.
Implement indexed(items): return a list of [index, value] pairs, numbering each item from 0.
Use enumerate.
For ["a", "b"] return [[0, "a"], [1, "b"]].
2 hints and 3 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Implement pair_totals(names, scores): return a list of [name, score] pairs by walking both
lists together with zip.
(["a", "b"], [1, 2]) returns [["a", 1], ["b", 2]].
2 hints and 3 automated checks are waiting in the workspace.