Blog

enumerate and zip in Python

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 of range(len(x)).

  • zip truncates to the shortest input in silence. Pass strict=True when 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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.