Topic 2.6
Looping Like a Pythonista: enumerate and zip
In one line
enumerate gives you the position and the item together, zip walks several sequences in step, and reversed and sorted change the order. They replace the beginner pattern for i in range(len(x)).
Think of it like this
A queue at a bank with numbered tokens. The clerk calls "Token 1: Asha. Token 2: Ben. Token 3: Chen." She reads two things together: the number and the person. enumerate hands you that pair each time, so you never keep the count by hand.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- enumerate
- Pairs each item with its position:
(0, item0), (1, item1), .... - zip
- Walks several sequences together and gives tuples of matching items.
- Tuple
- A fixed group of values like
("Asha", 90)(Topic 4.3). - Unpacking
- Splitting a pair or group into separate variables:
index, color = pair. - Sequence
- Items kept in a fixed order, such as a list, a string or a range.
- In place
- Changing the original itself instead of making a new copy.
- reversed
- Goes through a sequence backwards without changing it.
- sorted
- Returns a new list in order and leaves the original alone.
- strict=True
- An option of
zip(Python 3.10) that raises an error if the sequences have different lengths. - Pythonic
- Written in the clear, standard style Python programmers expect.
Step by step
01The beginner way and the better way
Both loops print the same lines. The second does not need len, range or indexing, so there is nothing to get wrong.
colors = ["red", "green", "blue"]
# Noisy
for i in range(len(colors)):
print(i, colors[i])
# Better
for index, color in enumerate(colors):
print(index, color)02enumerate with start
People count from 1, so give start=1 for numbered lists shown to humans. This only changes the number, not the items. The f"..." text fills each {} hole with a value (Topic 1.9).
colors = ["red", "green", "blue"]
for number, color in enumerate(colors, start=1):
print(f"{number}. {color}")03zip: two lists in step
The first name goes with the first score, the second with the second. The pair is unpacked into name and score on each pass.
names = ["Asha", "Ben", "Chen"]
scores = [90, 72, 85]
for name, score in zip(names, scores):
print(name, "scored", score)04zip stops at the shortest
With three names and two scores, zip gives only two pairs and says nothing about Chen. No error, no warning: the data just vanishes.
names = ["Asha", "Ben", "Chen"]
scores = [90, 72]
print(list(zip(names, scores))) # [('Asha', 90), ('Ben', 72)]05strict=True makes a mismatch loud
With strict=True the same data raises ValueError. Because zip is lazy, the pairs that match are produced first, and the error appears when the loop reaches the extra item. The second runnable example shows this.
names = ["Asha", "Ben", "Chen"]
scores = [90, 72]
for name, score in zip(names, scores, strict=True):
print(name, score)06reversed and sorted
reversed walks backwards. sorted returns a new ordered list. The original is unchanged in both cases. Capital letters sort before lowercase ones, because their character codes are smaller.
colors = ["red", "green", "blue"]
for color in reversed(colors):
print("Backwards:", color)
print(sorted([3, 1, 2])) # [1, 2, 3]
print(sorted(["banana", "apple", "Cherry"])) # ['Cherry', 'apple', 'banana']
print(colors) # unchanged07Combining them
Nest the pair in brackets when you unpack enumerate(zip(...)): the outer pair is (position, pair), and the pair itself is (name, score).
names = ["Asha", "Ben", "Chen"]
scores = [90, 72, 85]
for place, (name, score) in enumerate(zip(names, scores), start=1):
print(place, name, score)Try it yourself
- 1
Numbered shopping list
Make a list
items = ["milk", "eggs", "bread"]and print it as1. milk,2. eggs,3. breadusingenumeratewithstart=1. - 2
Three lists
zipaccepts more than two sequences. Add a list ofages = [20, 31, 28]to the names and scores and print one line per person with all three values. - 3
Check the original
Call
sorted(colors)andreversed(colors)and then printcolors. It is stillred, green, blue. Now think about why returning a new list is the safer design.
Code & diagrams
Add a fourth color and a fourth score and run again. Then remove one score and see how zip quietly drops the extra name.
Expected output
0 red
1 green
2 blue
1. red
2. green
3. blue
Asha scored 90
Ben scored 72
Chen scored 85
Backwards: blue
Backwards: green
Backwards: red
[1, 2, 3]
['Cherry', 'apple', 'banana']
['Chen', 'Ben', 'Asha']
['red', 'green', 'blue']This program ends with an error on purpose. Read the last line, then add a third score (for example 85) so both lists have three items and the error disappears.
Expected output
[('Asha', 90), ('Ben', 72)]
Asha 90
Ben 72
Traceback (most recent call last):
File "main.py", line 4, in <module>
for name, score in zip(names, scores, strict=True):
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: zip() argument 2 is shorter than argument 1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Unpack into the wrong shape
Use two loop variables on a list of plain strings.
names = ["Asha", "Ben"]
for index, name in names:
print(index, name)Myth vs fact
Myth
for i in range(len(x)) is the normal way to loop with positions.
Fact
It is the beginner pattern. enumerate(x) is shorter, clearer and cannot index the wrong list.
Myth
sorted(x) sorts the list in place.
Fact
sorted returns a new list and leaves the original alone. The in-place version is the list method x.sort() (Topic 4.2).
Myth
zip raises an error if the lists have different lengths.
Fact
By default it stops silently at the shortest. Only strict=True (Python 3.10) raises an error.
Myth
enumerate must start at 0.
Fact
It starts at 0 by default, and start=1 (or any number) changes the first count.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
enumerate,zipandreversedreturn lazy iterators: no copy of the data is made, so they are as cheap on a million items as on three.sortedis different: it must build a full new list, in O(n log n) time (Topic 4.11). - ▸
ziphas an ordering subtlety with iterators: it pulls from its inputs left to right, so when the first input is the longer one, an item is taken from it and then lost when the next input runs out.a = iter([1, 2, 3]); b = iter([1, 2]); list(zip(a, b))leavesaempty, whilezip(b, a)leaves3ina. Put the shorter input first if you will reuse the leftover, or usestrict=True. - ▸
zip(*rows)(unpacking with*, Topic 5.3) transposes a table: rows become columns. Combined withdict(zip(keys, values)), it is the standard way to build a dictionary from two lists. - ▸
In data code,
zip(strict=True)catches real bugs, such as a features list and a labels list drifting out of step in a training script. Silent truncation there produces a model trained on mismatched data with no error to warn you. - ▸
itertools.zip_longestpads the shorter sequence instead of stopping (Topic 9.4), anditertools.pairwise(3.10) gives each item with its neighbour, the proper replacement forrange(len(x) - 1)loops.
Remember this
- 1
Beginners often write
for i in range(len(names)): print(i, names[i]). (names[i]means "the item at positioni".) It works, but it is noisy and easy to get wrong (an off-by-one, or indexing a different list). The standard Python way isfor i, name in enumerate(names):.enumerateyields pairs(position, item), andi, nameunpacks each pair into two variables (Topic 4.3 explains unpacking). Positions start at 0 unless you givestart=1. - 2
zipwalks two or more sequences side by side:zip(names, scores)gives("Asha", 90), then("Ben", 72), and so on. It is the answer to "I have two lists that belong together" (names and scores, questions and answers, keys and values).for name, score in zip(names, scores):reads like the problem. - 3
zipstops at the shortest sequence and silently drops the rest. That is a quiet source of lost data when two lists have different lengths by mistake. Python 3.10 addedzip(..., strict=True), which raisesValueError(the error for a right kind of value that is wrong in some way) if the lengths differ. Use it whenever the lists *should* be the same length, so a mismatch fails loudly. - 4
reversed(x)goes through a sequence (items in a fixed order, such as a list or a string) from the end to the start, andsorted(x)gives a new sorted list. Neither changes the original (they do not change it "in place").sorted(x, reverse=True)sorts from largest to smallest. Strings sort by character code, so capital letters come before lowercase ones ("Cherry"before"apple"); Topic 4.2 showskey=for custom order. - 5
These helpers combine.
for i, (name, score) in enumerate(zip(names, scores), start=1):numbers a leaderboard. They are all lazy likerange: they produce one item at a time instead of building a new list first, which is why they work on huge inputs. - 6
Use a raw
range(len(x))only when you really need the positions alone (for example, to compare neighboursx[i]andx[i + 1], or to change items in place). In nearly every other caseenumerateorzipis clearer and safer.
Explain it without notes
Why is enumerate preferred to range(len(items))?
What happens when you zip lists of different lengths, and how do you make it safe?
What is the difference between sorted(x) and x.sort()?
What does for i, (a, b) in enumerate(zip(xs, ys)) do?
Practice
Print the list ["milk", "eggs", "bread"] as 1. milk, 2. eggs, 3. bread using enumerate.
Given items = ["pen", "book", "bag"] and prices = [10, 45, 120], print pen costs 10 and so on using zip.
Print the numbers 1, 2, 3 in reverse order using reversed, one per line, and then print the sorted version of [5, 2, 9, 1] followed by the original list.
Trade-offs
- ↔
strict=True vs the default zip. Strict catches mismatched data but fails the program when the lengths legitimately differ. Use the default only when truncating is what you want.
- ↔
Lazy iterators vs lists.
zipandreversedsave memory but can be used once. If you need to loop twice, wrap the result inlist(...).
Done when you can
Done when you can replace
range(len(x))withenumerateand usestart=.Done when you can loop two lists together with
zipand unpack the pairs.Done when you can explain what
zipdoes with unequal lengths and usestrict=True.Done when you can say that
sortedandreverseddo not change the original.