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