PYTHON / LOOPS
zip() and parallel iteration
Use zip() to walk several sequences in lockstep, unpack the tuples it yields, and control what happens when the inputs have different lengths.
What you will learn
- Iterate two or more sequences in lockstep and unpack each tuple in the for header
- Predict that zip() stops at the shortest input and raises no error while doing it
- Transpose a table or unzip pairs with zip(*rows)
- Reach for zip_longest or strict=True when a length mismatch means a bug
Understanding zip() and parallel iteration
Related data often arrives split into separate lists: one list of names, one of scores, one of dates, all lined up by position. zip() gives you a row-wise view of that column-wise data. On each step it pulls exactly one item from every iterable you gave it and hands you those items as a tuple, so `for name, score in zip(names, scores)` unpacks that tuple straight into two loop variables. The number of names you unpack into must match the number of iterables you passed, otherwise you get a ValueError about unpacking.
zip() does not build a list. It returns a lazy iterator that computes the next tuple only when asked, which means you can zip a million-line file against a counter without allocating anything, but also that the object is single-use: once a loop or list() has drained it, iterating again yields nothing. The second consequence of the pull-one-from-each model is the shortest-wins rule. The instant any input is exhausted, zip stops and reports a normal end of iteration, so extra items in the longer inputs are dropped without a word.
Because zip's argument list is just a sequence of iterables, `zip(*rows)` turns a list of rows into columns: each step takes item 0 from every row, then item 1, and so on, which is a transpose. The same trick reads as "unzip" when you write `labels, values = zip(*pairs)`. When silent truncation would hide a real problem, ask for the behaviour you want explicitly: `itertools.zip_longest` pads the short inputs with a fillvalue, and `zip(..., strict=True)` (Python 3.10 and later) raises ValueError if the inputs do not finish together.
names = ["ada", "grace", "alan"]
langs = ["Analytical Engine", "COBOL", "Turing machine"]
years = [1843, 1959, 1936]
for name, lang, year in zip(names, langs, years):
print(f"{name:6} {year} {lang}")
pairs = zip(names, years)
print(list(pairs))
print(list(pairs))zip() advances several iterables one step at a time, yielding a tuple per step, and quits as soon as the shortest one ends.
Worked examples
Shortest wins, and how to opt out
Shows that zip() silently drops trailing items and that zip_longest pads instead.
from itertools import zip_longest
hours = [9, 10, 11, 12]
temps = [14.1, 15.6, 17.0]
print(list(zip(hours, temps)))
print(list(zip_longest(hours, temps, fillvalue=None)))Example explained
Line 1zip stops the moment temps runs out, so hour 12 never reaches the first line of output.
Line 2No exception and no warning: the missing reading looks exactly like a three-hour dataset.
Line 3zip_longest keeps going until the longest input ends, substituting fillvalue for what is absent.
Line 4fillvalue already defaults to None, so passing it here only documents the intent.
Transposing and unzipping with zip(*data)
Uses argument unpacking to flip rows into columns and to split a list of pairs into two tuples.
rows = [(1, 2, 3), (4, 5, 6)]
print(list(zip(*rows)))
points = [("x", 10), ("y", 20), ("z", 30)]
labels, values = zip(*points)
print(labels)
print(values)
print(sum(values))Example explained
Line 1zip(*rows) calls zip((1, 2, 3), (4, 5, 6)), so each step collects one item per row: a transpose.
Line 2The columns come back as tuples, not lists; use [list(c) for c in zip(*rows)] if you need to mutate them.
Line 3labels, values = zip(*points) is the standard unzip: two names on the left, two tuples on the right.
Line 4sum(values) works because the numbers are now separated from their labels.
Catching a length mismatch with strict=True
Demonstrates that strict=True turns silent truncation into a ValueError, but only after the shared items are produced.
ids = [101, 102, 103]
emails = ["a@x.com", "b@x.com"]
try:
for user_id, email in zip(ids, emails, strict=True):
print(user_id, email)
except ValueError as err:
print("error:", err)Example explained
Line 1strict=True makes zip check, at the step where one input ends, that all the others ended too.
Line 2The first two pairs print normally; the check can only fail on the third step.
Line 3So partial work is already done when the ValueError arrives — strict is a detector, not a transaction.
Line 4The message names the offending argument by position, which is why arguments 1 and 2 are mentioned.
Important notes
The strict=True keyword exists only from Python 3.10; on older versions compare len() of the inputs first, or use zip_longest with a sentinel fillvalue.
When the inputs are generators rather than lists, the step that hits the end has already pulled items from the earlier iterables, and those items are discarded with the tuple that was never completed.
Common mistakes
Storing `pairs = zip(a, b)` and looping over it twice: the second loop body never runs, because the first loop exhausted the iterator, and nothing signals the problem.
Treating the result like a list — `zip(a, b)[0]` or `len(zip(a, b))` — which raises TypeError: 'zip' object is not subscriptable / has no len().
Assuming zip pads or complains when lists differ in length, so a short column quietly truncates the whole report and the missing rows are never noticed.
Try it yourself
Change, predict, then run
Given keys = ['host', 'port', 'debug'] and values = ['localhost', 8080], print dict(zip(keys, values)) and note which key vanished; then rebuild it with itertools.zip_longest so the missing setting appears with value None.
Open the Python workspaceCheck your understanding
What does `pairs = zip([1, 2, 3], "ab")` followed by `print(len(list(pairs)), len(list(pairs)))` print?
- 2 2
- 2 0
- 3 0
- It raises TypeError because a list and a string cannot be zipped together
Show answer
The string has only two characters, so shortest-wins gives two tuples on the first list() call. zip returns a one-shot iterator, so by the time the second list() runs it is already exhausted and produces an empty list, hence 0. "2 2" is tempting if you picture zip as returning a stored list of pairs, but nothing is stored — the tuples are generated on demand and gone once consumed.