Python Part I · Module 10 · Loops
enumerate() and zip()
Chapter 10.4 ended by saying that for i in range(len(x)) is almost always a sign you wanted something else. This chapter is the something else. Two situations account for nearly every index an engineer writes by hand: wanting a running number alongside each item, and wanting to walk two collections together because their entries correspond. Python supplies a helper for each, and both do more than shorten the code — they remove a class of mistake that a hand-maintained counter makes available. One of the two also has a sharp edge that silently discards data, which is worth more of your attention than its syntax.
Foundation16 min readPythonPart IFundamentalsLoopsenumeratezip
Part I · Module 10 · Chapter 10.9 · enumerate() and zip()
1. The Counter You Maintain by Hand
A numbered report needs a running number, and the obvious way to get one is to keep it yourself:
tests = ["smoke", "reset", "burst"]
i = 1
for test in tests:
print(i, test)
i += 11 smoke
2 reset
3 burstThree lines of that program are about the number rather than the work: create it, print it, advance it. Two of them can go wrong independently, and Section 5 shows one doing so.
2. enumerate()
tests = ["smoke", "reset", "burst"]
for i, test in enumerate(tests, start=1):
print(i, test)1 smoke
2 reset
3 burstenumerate() walks the collection and supplies a number alongside each item. There is no counter to create and none to advance — the number comes from the iteration itself.
Without start, it counts from zero, which is what you want when the number is a position:
tests = ["smoke", "reset", "burst"]
for i, test in enumerate(tests):
print(i, test)0 smoke
1 reset
2 burst3. What a Pass Hands You
enumerate() produces a two-field tuple per pass, and the two names before in unpack it — Chapter 6.5's mechanism again:
tests = ["smoke", "reset"]
print(list(enumerate(tests)))
print(list(enumerate(tests, start=1)))
for pair in enumerate(tests):
print(pair, type(pair))[(0, 'smoke'), (1, 'reset')]
[(1, 'smoke'), (2, 'reset')]
(0, 'smoke') <class 'tuple'>
(1, 'reset') <class 'tuple'>Taking it as a single name shows what is really being handed over; taking it as two is how you always write it.
4. Replacing range(len(...))
The pattern Chapter 10.4 warned about, and its replacement:
tests = ["smoke", "reset", "burst"]
for i in range(len(tests)):
print(i, tests[i])0 smoke
1 reset
2 bursttests = ["smoke", "reset", "burst"]
for i, test in enumerate(tests):
print(i, test)0 smoke
1 reset
2 burstThe second has no lookup, cannot go out of range, and keeps working if tests becomes a tuple or a string. Whenever range(len(...)) appears, enumerate() is the question to ask first.
5. The Mistake a Hand Counter Makes Available
The real argument for enumerate() is not brevity. It is that a counter you advance yourself can be advanced in the wrong place:
tests = ["smoke", "reset", "burst"]
i = 1
for test in tests:
if test.startswith("s"):
print(i, test)
i += 1
print("counter ended at", i)1 smoke
counter ended at 2The intent was number every test, print only the ones starting with s. The increment sits inside the if, so the counter advances only on matching items — it is now counting matches rather than position, and after three tests it reads 2.
This compiles, runs, raises nothing, and produces numbers that look plausible. With enumerate() it cannot happen, because the number is produced by the iteration and the body has no say in it:
tests = ["smoke", "reset", "burst"]
for i, test in enumerate(tests, start=1):
if test.startswith("s"):
print(i, test)1 smokeHere i is the position in the suite whatever the body does. If you wanted a count of matches instead, that is a separate counter and Chapter 10.10 is about keeping the two apart.
6. zip(): Two Collections Side by Side
Results often arrive as parallel lists — one of names, one of outcomes, in corresponding order:
tests = ["smoke", "reset", "burst"]
statuses = ["PASS", "FAIL", "PASS"]
for test, status in zip(tests, statuses):
print(test, "->", status)smoke -> PASS
reset -> FAIL
burst -> PASSzip() takes one item from each collection per pass and hands them over together. No index is involved, which means no index can be wrong.
It takes as many collections as you like:
tests = ["smoke", "reset"]
seeds = [4210, 4211]
statuses = ["PASS", "FAIL"]
for test, seed, status in zip(tests, seeds, statuses):
print(test, seed, status)smoke 4210 PASS
reset 4211 FAILAnd a zip of two is exactly the shape a dictionary wants:
tests = ["smoke", "reset"]
statuses = ["PASS", "FAIL"]
print(dict(zip(tests, statuses))){'smoke': 'PASS', 'reset': 'FAIL'}7. What zip() Hands You
A tuple per pass, one field per collection:
tests = ["smoke", "reset"]
statuses = ["PASS", "FAIL"]
print(list(zip(tests, statuses)))[('smoke', 'PASS'), ('reset', 'FAIL')]Which is why for test, status in zip(...) works: it is tuple unpacking, same as enumerate().
8. zip() Stops at the Shortest, Silently
This is the sharp edge, and it loses data without saying so:
tests = ["smoke", "reset", "burst"]
statuses = ["PASS", "FAIL"]
for test, status in zip(tests, statuses):
print(test, status)
print("paired:", len(list(zip(tests, statuses))), "of", len(tests))smoke PASS
reset FAIL
paired: 2 of 3Three tests, two statuses, two rows. "burst" is not in the output and nothing reported that it was dropped.
Consider what that means in a results pipeline. If the status list was built by a loop that skipped a crashed run, the lists are no longer parallel — and the report that zip() produces is not merely missing a row, it is wrong from the point of the mismatch onwards, because each remaining status is attached to the wrong test. A report that silently renames failures is worse than one that fails.
9. strict=True Turns It Into an Error
Since Python 3.10, zip() can insist that its arguments are the same length:
tests = ["smoke", "reset", "burst"]
statuses = ["PASS", "FAIL"]
for test, status in zip(tests, statuses, strict=True):
print(test, status)smoke PASS
reset FAIL
ValueError: zip() argument 2 is shorter than argument 1Two things worth noticing. The pairs that did match were produced before the error — zip() discovers the mismatch when it runs out, not in advance. And the message names which argument was short, which is the information you need.
Use strict=True whenever the collections are supposed to be the same length. That is almost always, and it converts a silent wrong answer into a loud stop. Leave it off only when truncation is genuinely what you want.
10. Both Together
Numbering paired rows needs both, and the unpacking needs brackets:
tests = ["smoke", "reset"]
statuses = ["PASS", "FAIL"]
for i, (test, status) in enumerate(zip(tests, statuses), start=1):
print(i, test, status)1 smoke PASS
2 reset FAILenumerate() hands over a pair whose second field is itself a pair, so the names must be grouped the same way: i takes the number, and (test, status) takes the tuple. Without the brackets Python has three names for two values and refuses.
Beyond this depth, the line stops being readable and a small loop body with explicit unpacking is better.
11. Predict, Run, Observe
# A
for i, ch in enumerate("abc"):
print(i, ch)
# B
for i, name in enumerate(["x", "y"], start=10):
print(i, name)
# C
a = [1, 2, 3]
b = ["one", "two"]
pairs = list(zip(a, b))
print("C:", pairs, len(pairs))
# D
keys = ["clk", "rst"]
vals = [1, 1]
print("D:", dict(zip(keys, vals)))The verified output:
0 a
1 b
2 c
10 x
11 y
C: [(1, 'one'), (2, 'two')] 2
D: {'clk': 1, 'rst': 1}A — a string is a collection, so enumerate() numbers its characters.
B — start sets where the numbering begins; it does not skip items.
C — two pairs from a three-item and a two-item list. The 3 is gone.
D — paired then built into a dictionary in one expression.
12. Exercises
Exercise 1 — number a report. Print 1. smoke, 2. reset, 3. burst from a list, using enumerate(). Then change it to print zero-based positions and say which you would show a user.
Exercise 2 — remove the index. Rewrite each without range(len(...)): (a) print each test with its position (b) print each line of a log prefixed with its line number starting at 1.
Exercise 3 — the misplaced increment. Run Section 5's broken program. Then move the increment out of the if and predict the new output before running. Explain in two sentences what each version is counting.
Exercise 4 — pair up. Given names = ["clk", "rst", "data"] and widths = [1, 1, 32], print clk is 1 bit(s) for each. Then build a dictionary from the two lists in one line.
Exercise 5 — find the dropped item. Given a three-item and a two-item list, use zip() and print how many pairs resulted. Name the item that was lost and explain how you would detect this in a program rather than by eye.
Exercise 6 — make it loud. Repeat Exercise 5 with strict=True, record the exact message, and say what the message tells you that the silent version did not.
Exercise 7 — three at once. Given test names, seeds and statuses as three parallel lists of equal length, print one line per run containing all three. Add strict=True and say why it costs nothing here.
Exercise 8 — combine. Print 1. smoke -> PASS and 2. reset -> FAIL from two parallel lists using enumerate() and zip() together. Then write the same thing as a loop over a list of tuples and say which you would rather maintain.
13. Summary
enumerate()supplies a running number alongside each item, with no counter to create or advance.start=1for numbers shown to people, the default zero for positions used as indices.- It hands you a two-field tuple per pass, unpacked by the two names before
in. - It replaces
for i in range(len(x)):— no lookup, no out-of-range, works on any collection. - A hand-maintained counter can be advanced in the wrong place and will then count matches instead of positions, silently.
enumerate()makes that impossible. zip()walks two or more collections together, handing over one item from each per pass.zip()stops at the shortest argument and says nothing — three tests and two statuses give two rows, and the third test vanishes.- A mismatch does not just drop a row; it misaligns every pair after it, which can attach the wrong status to the wrong test.
strict=Trueturns that into aValueErrornaming the short argument. Use it whenever the lists are supposed to match.- Combining them needs grouped names:
for i, (a, b) in enumerate(zip(x, y)):.
The next chapter is about the other thing a loop body does with a value it computes: keeping it.
Where this fits
Part of the Python curriculum.
