Skip to content
← All Python articles

Python explainer

Catch Mismatched Lengths with zip(strict=True)

About 4 min to read. Allow extra time to work through examples.

Plain zip drops the extra items when two lists differ in length. Add strict=True to get an error instead, and learn when that error appears.

This article uses Python. You can make it your practice mode to browse related content across the site.

You read two columns from a file, names and scores. One score is missing, so the scores column is a row short. You pair the columns with zip:

names = ["ann", "bob", "cy"]
scores = [91.0, 88.5]
print(dict(zip(names, scores)))  # {'ann': 91.0, 'bob': 88.5}

Cy is gone, and nothing told you. zip stops as soon as its shortest input runs out. That is what you want when you mean to cut the longer input short. When the two columns must match, it hides a bug.

Ask zip to check the lengths

Pass strict=True, and zip raises ValueError when one input runs out before the other:

pairs = zip(["ann", "bob", "cy"], [91.0, 88.5], strict=True)  # no error yet
print(next(pairs))  # ('ann', 91.0)
print(next(pairs))  # ('bob', 88.5)
try:
    next(pairs)
except ValueError as exc:
    print(exc)  # zip() argument 2 is shorter than argument 1

The error comes at the third next(), not at the call. zip is lazy. It makes a pair only when you ask for the next one, so it cannot know the lengths when you call it. It finds the mismatch while you loop, at the moment one input runs out. The rest of this track builds on this idea.

Two things follow. If you stop looping early, the check never runs. And when the check does raise, your loop has already handled every pair before the mismatch.

Turn the error into your own

The code that reads the file usually wants its own error, with both lengths in the message. Catch the ValueError and raise your own exception from it:

from collections.abc import Sequence


class ColumnMismatch(ValueError):
    pass


def scores_by_name(names: Sequence[str], scores: Sequence[float]) -> dict[str, float]:
    try:
        pairs = list(zip(names, scores, strict=True))
    except ValueError as exc:
        raise ColumnMismatch(f"{len(names)} names but {len(scores)} scores") from exc
    return dict(pairs)


print(scores_by_name(["ann", "bob", "ann"], [91.0, 88.5, 95.0]))
# {'ann': 95.0, 'bob': 88.5}

list() runs the zip to the end, so the length check always happens. The try block holds only the zip, so the only ValueError it can catch is the length mismatch. If the block also parsed scores with float(text), a bad score would come out as a length mismatch. raise ... from exc attaches zip's error as the cause, so the traceback shows both errors.

The Sequence hint, from collections.abc, covers any list-like input that has a length, such as a list or a tuple. The message calls len(), which an Iterable hint does not promise. And dict(pairs) keeps the last score for a repeated name, which is why ann ends up with 95.0.

Check lengths first, or reject repeated names

The function above suits most code. Two cases call for something else.

Compare the lengths before you start. In a loop that writes each pair to a database, strict zip raises only after the loop has written the earlier rows. When both inputs are lists, compare len(names) with len(scores) and raise before any work. A generator has no length, so for a generator strict zip is the check you have.

Reject repeated names. dict(pairs) drops the earlier score for a repeated name without telling you. If a repeat means the file is broken, check each name as you store it:

from collections.abc import Sequence


def unique_scores(names: Sequence[str], scores: Sequence[float]) -> dict[str, float]:
    result: dict[str, float] = {}
    for name, score in zip(names, scores, strict=True):
        if name in result:
            raise ValueError(f"duplicate name: {name}")
        result[name] = score
    return result

Pick the behavior you want

You want Use
Stop at the shortest input, on purpose zip(a, b, strict=False)
An error when the lengths differ zip(a, b, strict=True)
Every item of the longest input, with gaps filled itertools.zip_longest(a, b)
An error before any work, for lists Compare len(a) and len(b) first

strict=False does the same as plain zip. Writing it tells the next reader that you chose to drop the extra items.

from itertools import zip_longest

print(list(zip_longest(["ann", "bob", "cy"], [91.0, 88.5])))
# [('ann', 91.0), ('bob', 88.5), ('cy', None)]

zip_longest fills each gap with None, or with the value you pass as fillvalue. Pick a fill your code can tell apart from real data. None works for scores, but a fill of 0.0 would look like a real score.

Check your understanding You loop over zip(names, scores, strict=True) and break after the first pair. The columns have different lengths. Do you get an error? Show answer

No. zip checks the lengths only when one input runs out, and your loop stopped before that. The mismatch goes unnoticed. If you need the check, consume every pair, for example with list() or a loop that runs to the end.

Going further

strict=True arrived in Python 3.10. From Python 3.14, map takes strict=True too, as in map(pow, bases, exponents, strict=True).

With three or more inputs, strict zip reports the first mismatch it finds. It does not list the length of every input.

Equal lengths tell you the counts match. They do not tell you that row 3 of one file belongs with row 3 of the other. If both files carry an ID, pair the rows by ID instead of by position.

step 1 of 9

Generators, Iterators and Streaming Data

Up next Pair Names with Scores and Reject a Length Mismatch