Python zip: Why Evaluation Samples Disappear
There are three labels, only two model predictions, and an evaluation script reports 100% accuracy. The problem may precede the metric formula: ordinary zip stops when its shortest input ends. The missing prediction never becomes a comparison. When equal-length inputs are required, Python 3.10 and later offer zip(..., strict=True) to turn that silent truncation into an error.
Contents
Strict mode is neither complete data validation nor a transaction. It detects a length mismatch when iteration reaches it, cannot undo earlier writes, and accepts equally long inputs in the wrong order. Reliable evaluation needs separate rules for quantity, sample identity, and result publication.
A perfect score can describe an incomplete prefix
Use a deliberately synthetic example: labels [1, 0, 1] and predictions [1, 0]. Ordinary zip produces two pairs, both matching. Dividing by the number of pairs gives 2/2; dividing the same two correct comparisons by the original label count gives 2/3. No model behavior changed. The denominator and treatment of missing output changed.
It would be incorrect to declare 2/3 the repaired true accuracy automatically. Whether a missing prediction counts as failure or is excluded must come from a previously specified evaluation protocol. If every sample requires a prediction, reject the incomplete input before publishing a score instead of selecting a number that merely looks more conservative.
The same pairing problem can affect training examples and weights, retrieved documents and relevance labels, or request IDs and asynchronous results. zip cannot know the intended evaluation population or whether a missing item is acceptable. The application must express that the inputs represent the same batch.
Strict checking occurs during consumption
PEP 618 introduced the optional strict argument for Python 3.10. The default truncating behavior remains unchanged. Selecting strict mode still creates an iterator, not a container whose entire input has already been validated.
Constructing the iterator therefore does not establish equal lengths. In the example, the first two next calls produce valid pairs. The third discovers that one input ended first and raises ValueError. Reading only the first pair and stopping would not validate the remaining batch, even though no error had occurred.
For a stream, this timing is understandable: the end may be unknown until it is reached. For an evaluator, it means partial results cannot be published on the assumption that a later length check will validate them retroactively. Detecting a contract violation and controlling earlier effects are separate responsibilities. A failed iterator is not automatically rewound: retries need a reconstructible source or an explicit recovery position.
A detected error does not undo earlier writes
When a loop appends each pair immediately, the first two writes precede the exception. They remain in the list after the exception is caught. The same control-flow issue matters when the loop appends to a file, writes a database row, or sends a message: strict mode supplies no rollback.
For a small finite batch, fully materializing the strict pairs before committing downstream results can separate these phases. If collection fails, assignment does not complete and the else branch is skipped. The committed list in the example stays empty. This protects the particular downstream write placed after validation; it does not implement transactions for arbitrary external systems.
Consuming an upstream iterator may itself read files, call services, or produce other effects. Staging does not reverse those actions, and materializing all pairs uses memory proportional to the batch size. Large files or online streams require an explicit publication boundary, such as temporary output, transactional storage, or a reversible batch protocol. A strict pairing flag is not evidence that an entire pipeline is atomic.
A runnable example checks identity separately
Save the following as zip_strict_example.py and run it with Python 3.10 or later. The actual local run used Python 3.15.0. Integers and Fraction keep the teaching arithmetic exact. No model was invoked and no real user data was used. Do not use -O, because the assertions verify the demonstration.
from itertools import zip_longest
from fractions import Fraction
labels = [1, 0, 1]
predictions = [1, 0]
pairs = list(zip(labels, predictions))
assert len(pairs) == 2
assert Fraction(sum(y == p for y, p in pairs), len(pairs)) == 1
assert Fraction(sum(y == p for y, p in pairs), len(labels)) == Fraction(2, 3)
# strict checking happens as iteration reaches the length mismatch.
z = zip(labels, predictions, strict=True)
assert next(z) == (1, 1)
assert next(z) == (0, 0)
try:
next(z)
except ValueError:
pass
else:
raise AssertionError('length mismatch not detected')
written = []
try:
for pair in zip(labels, predictions, strict=True):
written.append(pair)
except ValueError:
pass
assert written == [(1, 1), (0, 0)] # Earlier effects remain.
committed = []
try:
staged = list(zip(labels, predictions, strict=True))
except ValueError:
pass
else:
committed.extend(staged)
assert committed == []
truth = [('a', 1), ('b', 0)]
reordered = [('b', 0), ('a', 1)]
wrong_pairs = list(zip(truth, reordered, strict=True))
assert len(wrong_pairs) == 2 # Equal length does not establish identity.
assert any(a[0] != b[0] for a, b in wrong_pairs)
def checked_score(truth_rows, prediction_rows):
hits = count = 0
for (truth_id, y), (prediction_id, p) in zip(
truth_rows, prediction_rows, strict=True):
if truth_id != prediction_id:
raise ValueError('sample IDs differ')
hits += y == p
count += 1
if count == 0:
raise ValueError('empty evaluation')
return Fraction(hits, count)
assert checked_score(truth, truth) == 1
for bad in [reordered, truth[:1], []]:
try:
checked_score(truth, bad)
except ValueError:
pass
else:
raise AssertionError('invalid input accepted')
try:
checked_score([], [])
except ValueError:
pass
else:
raise AssertionError('empty evaluation accepted')
missing = object()
rows = list(zip_longest([None, 1], [None], fillvalue=missing))
assert rows[0] == (None, None)
assert rows[1][1] is missing
print('truncation, delayed error, partial effects, staging, IDs, missing: passed')
The truth rows are ordered a, b, while reordered contains b, a. Strict zip can consume both inputs completely because their lengths match. It still pairs different samples. checked_score compares IDs before labels, keeping identity separate from length, and rejects an empty evaluation rather than silently assigning it a score.
The function returns a metric only after the loop completes successfully. A mismatch can occur after local counters have increased, but the caller receives no partial score. That local property is preferable to catching the exception and reporting whatever the counters happened to contain.
This is not a general evaluation platform. It does not enforce global ID uniqueness: matching duplicate IDs in both inputs would pass the positional check. It also does not validate label domains or handle distributed shards. Those are additional input-contract requirements, not properties supplied by strict mode.
When missing rows matter, represent them explicitly
Some tasks should retain unmatched tails for a report instead of rejecting them. itertools.zip_longest can fill missing positions, but it does not choose whether a missing prediction means failure, retry, or exclusion. It does not establish sample order either.
The example uses a dedicated object sentinel and tests it with is. A legitimate None value is thereby distinguishable from a missing item. Reusing a valid data value as the placeholder can make a reporting layer interpret absence as an ordinary prediction. Missing output should be an explicit state with a documented meaning.
Results arriving in different orders often require alignment by stable IDs, with separate policies for duplicate, missing, and extra IDs. Filling positional tails does not repair a permutation. If retries are allowed, merging must also prevent multiple responses for one sample from becoming multiple independent evaluation samples.
Establish the pairing contract before interpreting metrics
Before attributing a higher score to a better model, identify the population actually scored. Does it match the expected batch? Is output complete? Do IDs correspond one to one? How are failed samples handled? Equal lengths answer only one of these questions. Accuracy, F1, and other summaries become comparable only after this input boundary is defined.
This complements fixing the class set in F1 evaluation. That article concerns the categories included in metric aggregation; this one concerns the samples paired before the metric runs. Either layer can change the score without improving predictions.
Strict mode is a clear contract check for finite inputs that must have equal lengths. If a shortest-prefix operation is intentional, ordinary zip can be correct, provided the truncation policy is explicit. Decide what the application must preserve before choosing the iterator; do not let a default stopping rule choose the evaluation population.
阅读完整中文版. This article retains its planned date of October 27, 2026, 23:30 Beijing time and was actually backfilled on October 29, 2026. PEP 618 dates from 2020; the feature predates that planned slot and is not presented as new-version news.
Reference: PEP 618: optional length checking for zip. The data, counterexamples, and two diagrams are independent teaching examples, not model benchmark results.
If these reproducible checks helped locate silent sample loss or partial writes, you are welcome to support future technical writing voluntarily.
Support
If the explanations or examples helped you solve a problem or save time, you’re welcome to support this site with a voluntary tip.
Click a QR code to enlarge. More options: support page。


