LESSON 22 · NEXT STEPS24 min

Loop with enumerate() and zip()

Pair related values and number them without manually managing an index.

Before you start: Lists · Loops · Functions

Video lesson: Loop with enumerate() and zip()

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    We have two names but only one score. What should a report do? It could quietly show only Ada, or it could tell us the data does not match. For a score report, silently losing Bo would be a serious mistake. Python gives us useful tools to pair values and number rows, but their defaults matter. zip takes corresponding items from two sequences. enumerate supplies a number with each item. Combining them makes a compact loop, yet a compact loop can still produce incomplete output. Before the terminal runs, predict whether it will print one row, two rows, or an error. We will run a broken version first, inspect the actual output, then make the mismatch visible and build a checked solution.

  2. 00:45

    The companion lesson has a browser console where you can change the lists and test your prediction. Try to distinguish what zip does from what enumerate does; the repair will become much easier once those two jobs are separate in your mind. Imagine names and scores coming from different files. The lists can have matching lengths most days and diverge after one record is skipped. A test with only perfect data would miss the problem. Our opening case is deliberately small enough that you can count both inputs and notice the missing partner yourself. The labels look neatly numbered, but the report contains only Ada. Our two names have one score, and ordinary zip stops when that shorter input ends.

  3. 01:29

    The loop never sees Bo. enumerate is doing its job correctly: it attaches a one-based number to each pair it receives. The missing row is caused by zip's default behavior, not by the numbering. This is the dangerous kind of bug that produces plausible output and no traceback. If both lists were the same length, a loop could unpack each name and score directly, and the labels would be complete. With unequal inputs, we need to reject the data before publishing a misleading report. zip is lazy; it forms pairs as the loop consumes them, and it does not modify the source lists. Think about where a length mismatch will become visible if we ask zip to be strict.

  4. 02:12

    Constructing the iterator alone is not enough; the loop or list conversion has to reach the missing partner. You could also index both lists with a range, but then the loop has to manage positions and can raise an index error at a different place. Pairing values with zip states the relationship directly. Adding strict mode keeps that readability while making a missing partner an explicit data problem. Strict zip refuses to finish quietly when one iterable is longer. We used `list` to consume the iterator; that is the point where it discovers that a second name has no score. The `try` block catches the error only for this demonstration.

  5. 02:53

    In the score-label function we will let it propagate so the caller can correct its input. Be careful with this distinction: `zip(..., strict=True)` creates an iterator, while iterating or converting it to a list actually checks all the rows. If you remove `strict=True` here, the second name disappears again. If you add Bo's score back, strict zip completes normally and yields both pairs. The rule is simple: use ordinary zip when truncation is intentional; use strict zip when every item needs a partner. The mismatch can occur in either direction. A second score with only one name also needs to be rejected. strict zip checks for both kinds of unequal length as the iterator is consumed.

  6. 03:39

    This is especially valuable when the inputs are generators, where checking len before looping may not even be possible. enumerate supplies a counter along with each item. By default it starts at zero; `start=1` makes numbered output friendlier to a reader. The first yielded value is the number and the second is the topic. We could create and increment a counter ourselves, but that adds a place for an off-by-one mistake. Now combine enumerate with zip. Think from the inside outward: zip produces a name-and-score pair. enumerate attaches a row number to that pair. That means each iteration has an outer pair containing the number and an inner pair containing the name and score.

  7. 04:24

    The loop header can unpack both levels: `for number, (name, score) in ...`. Read those parentheses as the structure of the data rather than punctuation to memorize. Watch the two levels of unpacking carefully. number comes from enumerate; name and score come from zip. If you swap those names or omit the inner parentheses, the program may report a confusing unpacking error. When a loop header looks dense, print one tuple from the iterator and inspect its shape before writing the body. The function returns labels instead of printing from inside. That lets a caller display the report or reuse the list elsewhere, and it lets a test compare exact labels.

  8. 05:06

    The two empty lists are a useful boundary case: there are no pairs and the function returns an empty list. An unequal pair raises ValueError because strict zip is consumed by the loop. On PythonLessonLab, the practical task asks you to write this function yourself. Start with a two-row example, then test an empty pair and a mismatch. The quiz checks what enumerate yields and why strict zip matters. After you pass both task and quiz, try changing the code to number from zero and explain the difference. Keep the source lists unchanged. The useful pattern from this lesson is not a complicated one-liner: it is a readable loop where the iterator supplies exactly the structure you need.

  9. 05:51

    After the basic checks pass, try one extra score and confirm that it also raises ValueError. Then rerun with three matching rows and verify the third label says 3, not 2. Those two tests protect against both silent truncation and an off-by-one numbering mistake without changing the original input lists.

Understand the concept

When you need both an item and its position, enumerate(items) yields pairs of index and item. Its first index is 0 unless you pass start=1. This is clearer than keeping a counter that you increment by hand, and it avoids changing the counter at the wrong point in a loop.

zip(names, scores) takes one item from each iterable per step. The loop variables can unpack each pair directly. Ordinary zip stops as soon as the shortest iterable ends, so an extra item in a longer list disappears silently. If losing an item would be a bug, use zip(..., strict=True) in Python 3.10 or later. A length mismatch then raises ValueError.

You can combine the tools: enumerate(zip(names, scores, strict=True), start=1) gives a row number and a name/score pair. Unpack them with for number, (name, score) in ... . Read the nested pair one layer at a time before writing the loop body.

The browser task returns formatted labels rather than printing from inside the function. Returning a list lets the caller display it and lets the checker test an empty input, ordering, and mismatched lengths. Start with two equal-length lists and predict the output before changing the data.

  • enumerate yields index/item pairs
  • zip pairs corresponding items
  • strict=True catches unequal lengths
  • start=1 changes displayed numbering

See it step by step

Read the code, predict the output, then compare it with the result.

01. Number a list from one

PYTHON
topics = ["loops", "functions", "files"]
for number, topic in enumerate(topics, start=1):
    print(f"{number}. {topic}")
EXPECTED OUTPUT
1. loops
2. functions
3. files

enumerate supplies each index and topic. start=1 changes presentation without changing the list itself.

02. Pair names with scores

PYTHON
names = ["Ada", "Bo"]
scores = [8, 5]
for name, score in zip(names, scores):
    print(f"{name}: {score}")
EXPECTED OUTPUT
Ada: 8
Bo: 5

Each iteration receives the corresponding name and score. The pair can be unpacked directly in the for statement.

03. Catch a missing partner

PYTHON
names = ["Ada", "Bo"]
scores = [8]
try:
    list(zip(names, scores, strict=True))
except ValueError as error:
    print(type(error).__name__)
EXPECTED OUTPUT
ValueError

strict=True reports the mismatch. list consumes the zip iterator so the check actually runs.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Let enumerate own the counter

A manual counter works until someone moves the increment to the wrong side of a print statement or forgets it in one branch. enumerate makes the pairing part of the iteration. The first value it yields is the index; the second is the item. You can unpack both directly in the for statement and name them for what they represent. It leaves the original list unchanged, so another part of your program can still use that list afterward.

The default index starts at zero because that is how Python indexes collections. A numbered report usually starts at one. Pass start=1 for display rather than adding one inside the loop each time. The call changes the numbers yielded by enumerate, not the indexes of the underlying list. Predict the first and last label in the example, run it, and then remove start=1 to see exactly what changed.

PYTHON
topics = ["loops", "functions", "files"]
for number, topic in enumerate(topics, start=1):
    print(f"{number}. {topic}")
print(topics[0])
EXPECTED OUTPUT
1. loops
2. functions
3. files
loops
Follow the reasoning
  1. enumerate yields (1, 'loops'), then (2, 'functions'), then (3, 'files').
  2. The underlying topics list still uses ordinary zero-based indexing.
  3. Remove start=1 and predict the three labels before running again.
02 / 03

Pair related sequences without losing a row

zip visits several iterables together. On each step it yields a tuple containing one value from each source. The loop can unpack the tuple as name and score. That is a natural fit when separate sequences represent columns of the same table. The order of the pairs follows the original order of each input, and zip does not modify either list. It produces an iterator, so the pairing happens as the iterator is consumed.

Ordinary zip stops when the shortest iterable ends. In the second example, Bo has no score and disappears from the paired output. That might be intended for some algorithms, but it is a data-loss bug for a score report. Python 3.10 introduced strict=True: when the iterator reaches that mismatch, it raises ValueError. You must consume the iterator to observe the error; constructing zip alone is lazy. Use strict mode whenever every row needs a partner.

PYTHON
names = ["Ada", "Bo"]
scores = [8]
print(list(zip(names, scores)))
try:
    print(list(zip(names, scores, strict=True)))
except ValueError as error:
    print(type(error).__name__)
EXPECTED OUTPUT
[('Ada', 8)]
ValueError
Follow the reasoning
  1. Ordinary zip pairs Ada with 8 and silently leaves Bo out.
  2. strict=True raises only as the list call consumes the zip iterator.
  3. Try adding a second score and confirm both rows appear without an error.
03 / 03

Unpack a numbered pair one layer at a time

Combining enumerate and zip creates a nested value: a row number plus a pair from the two sources. The loop header number, (name, score) mirrors that structure. First read zip(names, scores, strict=True) as a stream of (name, score) pairs. Then read enumerate(..., start=1) as a stream of (number, pair) pairs. Parentheses around name and score tell Python to unpack that inner pair as well.

A returned list of labels is easier to test than a function that only prints. It can be checked for ordering, exact formatting, and empty input without inspecting a terminal by eye. With two empty inputs, the loop makes no labels and returns an empty list. With unequal lengths, strict zip raises ValueError during iteration, which prevents a report that quietly omits somebody. The exercise asks you to preserve those behaviors.

PYTHON
def label_scores(names, scores):
    labels = []
    for number, (name, score) in enumerate(zip(names, scores, strict=True), start=1):
        labels.append(f"{number}. {name}: {score}")
    return labels

print(label_scores(["Ada", "Bo"], [8, 5]))
print(label_scores([], []))
EXPECTED OUTPUT
['1. Ada: 8', '2. Bo: 5']
[]
Follow the reasoning
  1. zip supplies each name and score pair.
  2. enumerate attaches the one-based row number.
  3. The function returns labels, including [] when there are no rows.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete label_scores(names, scores). Return a list such as ['1. Ada: 8', '2. Bo: 5'] using enumerate and zip. Start numbering at 1, keep input order, return [] for two empty lists, and let strict zip raise ValueError when lengths differ.
Need a hint?

Loop over enumerate(zip(names, scores, strict=True), start=1). Unpack the row as number, (name, score), append an f-string to a result list, and return the list.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What does enumerate(['a', 'b'], start=1) yield?

    02.What happens when ordinary zip receives lists of unequal lengths?

    03.How can zip detect a missing item in Python 3.10+?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Treating enumerate's first value as the item

    COMMON MISTAKE
    for topic, number in enumerate(['loops'], start=1):
        print(topic.upper())
    FIX
    for number, topic in enumerate(['loops'], start=1):
        print(topic.upper())

    What happens: AttributeError because topic is the integer index.
    enumerate gives index first, item second. Name the unpacked values in that order.

    Silently dropping an extra score

    COMMON MISTAKE
    list(zip(['Ada'], [8, 5]))
    FIX
    list(zip(['Ada'], [8, 5], strict=True))

    What happens: The second score is lost and no exception explains the mismatch.
    Use strict=True when both sequences must have matching lengths; consume the iterator to trigger the check.

    Browse this lesson’s problem fixes

    Sources and further reading