Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~24 min· topic 6 of 9

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.

main.pywhole filepython
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).

main.pywhole filepython
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.

main.pywhole filepython
names = ["Asha", "Ben", "Chen"]
scores = [90, 72, 85]
for name, score in zip(names, scores):
    print(name, "scored", score)
zip: two lists in stepdiagram
Rendering diagram…

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.

main.pywhole filepython
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.

main.pywhole filepython
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.

main.pywhole filepython
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)                                  # unchanged

07Combining them

Nest the pair in brackets when you unpack enumerate(zip(...)): the outer pair is (position, pair), and the pair itself is (name, score).

main.pywhole filepython
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. 1

    Numbered shopping list

    Make a list items = ["milk", "eggs", "bread"] and print it as 1. milk, 2. eggs, 3. bread using enumerate with start=1.

  2. 2

    Three lists

    zip accepts more than two sequences. Add a list of ages = [20, 31, 28] to the names and scores and print one line per person with all three values.

  3. 3

    Check the original

    Call sorted(colors) and reversed(colors) and then print colors. It is still red, green, blue. Now think about why returning a new list is the safer design.

Code & diagrams

enumerate, zip, reversed and sorted New tab

Add a fourth color and a fourth score and run again. Then remove one score and see how zip quietly drops the extra name.

Sign in to run this example in your browser.

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']
zip stops quietly, strict=True fails loudly Python 3.10+ New tab

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.

Sign in to run this example in your browser.

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 1

Break 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.

main.pywhole filepython
names = ["Asha", "Ben"]
for index, name in names:
    print(index, name)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 2, in <module>
for index, name in names:
^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)

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, zip and reversed return lazy iterators: no copy of the data is made, so they are as cheap on a million items as on three. sorted is different: it must build a full new list, in O(n log n) time (Topic 4.11).

  • ▸

    zip has 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)) leaves a empty, while zip(b, a) leaves 3 in a. Put the shorter input first if you will reuse the leftover, or use strict=True.

  • ▸

    zip(*rows) (unpacking with *, Topic 5.3) transposes a table: rows become columns. Combined with dict(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_longest pads the shorter sequence instead of stopping (Topic 9.4), and itertools.pairwise (3.10) gives each item with its neighbour, the proper replacement for range(len(x) - 1) loops.

Remember this

  1. 1

    Beginners often write for i in range(len(names)): print(i, names[i]). (names[i] means "the item at position i".) It works, but it is noisy and easy to get wrong (an off-by-one, or indexing a different list). The standard Python way is for i, name in enumerate(names):. enumerate yields pairs (position, item), and i, name unpacks each pair into two variables (Topic 4.3 explains unpacking). Positions start at 0 unless you give start=1.

  2. 2

    zip walks 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. 3

    zip stops 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 added zip(..., strict=True), which raises ValueError (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. 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, and sorted(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 shows key= for custom order.

  5. 5

    These helpers combine. for i, (name, score) in enumerate(zip(names, scores), start=1): numbers a leaderboard. They are all lazy like range: they produce one item at a time instead of building a new list first, which is why they work on huge inputs.

  6. 6

    Use a raw range(len(x)) only when you really need the positions alone (for example, to compare neighbours x[i] and x[i + 1], or to change items in place). In nearly every other case enumerate or zip is clearer and safer.

Explain it without notes

01

Why is enumerate preferred to range(len(items))?

02

What happens when you zip lists of different lengths, and how do you make it safe?

03

What is the difference between sorted(x) and x.sort()?

04

What does for i, (a, b) in enumerate(zip(xs, ys)) do?

Practice

01

Print the list ["milk", "eggs", "bread"] as 1. milk, 2. eggs, 3. bread using enumerate.

02

Given items = ["pen", "book", "bag"] and prices = [10, 45, 120], print pen costs 10 and so on using zip.

03

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. zip and reversed save memory but can be used once. If you need to loop twice, wrap the result in list(...).

Done when you can

  • Done when you can replace range(len(x)) with enumerate and use start=.

  • Done when you can loop two lists together with zip and unpack the pairs.

  • Done when you can explain what zip does with unequal lengths and use strict=True.

  • Done when you can say that sorted and reversed do not change the original.