These two come first because you write them daily, and because the last thing zip does in this post is the question the rest of the series answers.
The counter you keep writing
names = ['ada', 'grace', 'alan']
i = 0
for name in names:
print(i, name)
i += 1
It prints:
0 ada
1 grace
2 alan
Three lines of bookkeeping around one line of work. enumerate does all of it:
names = ['ada', 'grace', 'alan']
for i, name in enumerate(names):
print(i, name)
It prints:
0 ada
1 grace
2 alan
enumerate hands you a pair each time round: the count, then the item.
start changes the number, not the position
names = ['ada', 'grace', 'alan']
for n, name in enumerate(names, start=1):
print(f"{n}. {name}")
It prints:
1. ada
2. grace
3. alan
This is the one people reach for and then write i + 1 instead. start does not skip anything — it only changes the first number handed out.
Stop indexing by hand
names = ['ada', 'grace', 'alan']
for i in range(len(names)): # you index on every line that uses the item
print(i, names[i].upper())
for i, name in enumerate(names): # the item is already unpacked
print(i, name.upper())
It prints:
0 ADA
1 GRACE
2 ALAN
0 ADA
1 GRACE
2 ALAN
Same output, and the second one cannot get the index wrong. range(len(x)) is worth treating as a small alarm: it usually means enumerate, and sometimes means you did not need the index at all.
zip walks two sequences together
names = ['ada', 'grace', 'alan']
langs = ['analytical engine', 'cobol', 'turing machine']
for name, lang in zip(names, langs):
print(f"{name:6} {lang}")
It prints:
ada analytical engine
grace cobol
alan turing machine
zip stops at the shortest input and says nothing
This is the one that costs people an afternoon.
names = ['ada', 'grace', 'alan']
scores = [90, 85]
pairs = list(zip(names, scores))
print(pairs)
print(len(names), len(scores), '->', len(pairs))
It prints:
[('ada', 90), ('grace', 85)]
3 2 -> 2
Alan is gone. No error, no warning, just two rows where you expected three. If the two lists are supposed to be the same length, say so and let Python check:
names = ['ada', 'grace', 'alan']
scores = [90, 85]
try:
print(list(zip(names, scores, strict=True)))
except ValueError as err:
print('ValueError:', err)
It prints:
ValueError: zip() argument 2 is shorter than argument 1
strict=True needs Python 3.10 or newer. Use it whenever the lengths matching is an assumption rather than a coincidence.
Two things worth knowing
A zip of keys and values is a dict:
cols = ['id', 'name', 'role']
row = [17, 'ada', 'engineer']
print(dict(zip(cols, row)))
It prints:
{'id': 17, 'name': 'ada', 'role': 'engineer'}
And zip(*pairs) unzips, because unpacking the list of pairs makes each pair a separate argument:
pairs = [('ada', 90), ('grace', 85), ('alan', 71)]
names, scores = zip(*pairs)
print(names)
print(scores)
print(sum(scores) / len(scores))
It prints:
('ada', 'grace', 'alan')
(90, 85, 71)
82.0
You get tuples back, not lists. That surprises people once.
Both together
names = ['ada', 'grace', 'alan']
scores = [90, 85, 71]
for n, (name, score) in enumerate(zip(names, scores), start=1):
print(f"{n}. {name:6} {score}")
It prints:
1. ada 90
2. grace 85
3. alan 71
The parentheses around (name, score) are required. enumerate gives you two things, and the second one is itself a pair.
Neither of them gives you a list
names = ['ada', 'grace', 'alan']
scores = [90, 85, 71]
z = zip(names, scores)
print(list(z))
print(list(z))
It prints:
[('ada', 90), ('grace', 85), ('alan', 71)]
[]
The second list(z) is empty. Nothing was deleted and nothing failed — zip handed back something that can be walked once, and you walked it. enumerate, map, filter and an open file all behave the same way.
That is not a quirk of zip. It is how iteration works everywhere in Python, and it is the next post.
What to remember
-
enumerate(x, start=1)instead of a hand-rolled counter, and instead ofrange(len(x)). -
ziptruncates to the shortest input in silence. Passstrict=Truewhen equal lengths are an assumption. -
dict(zip(keys, values))builds a dict;zip(*pairs)takes one apart, into tuples. -
Both return something you can walk once. Wrap it in
list()the moment you need it twice.