Using Python itertools zip_longest for Uneven Iterables
python itertools zip_longest: Learn how itertools.zip_longest combines iterables of unequal length, controls missing values with fillvalue, and when to use it over zip.
When you combine two or more iterables with zip, iteration stops as soon as the shortest iterable is exhausted. That behavior is correct for many tasks, but it silently drops data when the iterables have unequal lengths. python itertools zip_longest solves that by continuing until every iterable is exhausted, filling missing values with a placeholder.
What zip_longest Solves That zip Does Not
Consider two lists of different lengths:
names = ["alice", "bob", "carol"] scores = [88, 92] list(zip(names, scores))
The result is [("alice", 88), ("bob", 92)]. The third name, "carol", is silently omitted. If you need to keep all data and explicitly mark the missing score, zip is the wrong tool.
itertools.zip_longest produces tuples for every position, using a fill value for the missing entries:
from itertools import zip_longest list(zip_longest(names, scores))
Output:
[("alice", 88), ("bob", 92), ("carol", None)]
The default fill value is None, but you can change it with the fillvalue parameter.
Syntax and Parameters of zip_longest
zip_longest(*iterables, fillvalue=None) accepts any number of iterables. It returns an iterator that yields tuples, one per position, until the longest iterable is exhausted. The function is part of the itertools module, so you must import it before use.
from itertools import zip_longest letters = ["a", "b", "c", "d"] numbers = [1, 2] for pair in zip_longest(letters, numbers): print(pair)
Output:
("a", 1) ("b", 2) ("c", None) ("d", None)
The fillvalue parameter accepts any Python object, not just None. This is useful when you need a specific sentinel value that cannot be confused with real data.
Using fillvalue to Control Missing Values
When None is not an appropriate placeholder, pass a custom fillvalue. For example, when aligning time series data, you might want to use 0 for missing numeric values:
from itertools import zip_longest quarters = ["Q1", "Q2", "Q3", "Q4"] revenue = [120, 150, 130] list(zip_longest(quarters, revenue, fillvalue=0))
Result:
[("Q1", 120), ("Q2", 150), ("Q3", 130), ("Q4", 0)]
A more robust approach is to use a sentinel object that is guaranteed not to appear in the data:
_MISSING = object() for item in zip_longest(iter_a, iter_b, fillvalue=_MISSING): if item[1] is _MISSING: # handle missing value explicitly pass
This avoids ambiguity when None or 0 could be legitimate data.
Working with Iterables of Different Lengths
zip_longest is not limited to two iterables. You can pass three or more, and the fill value applies to any iterable that runs out early.
from itertools import zip_longest headers = ["id", "name", "age"] rows = [ (1, "alice", 30), (2, "bob"), (3, "carol", 25, "extra"), ] for row in zip_longest(*rows, fillvalue="N/A"): print(row)
The output depends on the structure of rows. Here, each tuple in rows is treated as a separate iterable, so the result is:
(1, 2, 3) ("alice", "bob", "carol") (30, "N/A", 25) ("N/A", "N/A", "extra")
This is a common pattern for transposing ragged data. The fillvalue ensures every output tuple has the same length, which is useful when building tables or CSV rows.
Handling Infinite Iterators Carefully
zip_longest stops when the longest iterable is exhausted. If one of the iterables is infinite, zip_longest will never finish on its own. For example:
from itertools import count, zip_longest finite = ["a", "b", "c"] infinite = count() # 0, 1, 2, ... for pair in zip_longest(finite, infinite): print(pair) # runs forever
This is a common source of hangs. If you need to combine a finite iterable with an infinite one, use zip instead, which stops at the finite iterable's end, or add an explicit break condition. If you truly need zip_longest with an infinite iterable, you must break out of the loop manually based on your own logic.
Performance and Memory Considerations
zip_longest returns an iterator, so it does not build a list of all tuples in memory unless you call list() on it. The per-iteration overhead is slightly higher than zip because it must check whether each iterable is still active and apply the fill value when needed. For most workloads this difference is negligible, but if you are processing millions of rows, the extra function call overhead may matter.
If you only need the first few tuples, use islice to limit consumption:
from itertools import islice, zip_longest for pair in islice(zip_longest(long_a, long_b), 10): process(pair)
This avoids iterating over the entire dataset when you only need a prefix. Also note that zip_longest does not copy the input iterables; it consumes them as it goes. If you need to reuse an iterable later, convert it to a list first.
Choosing Between zip and zip_longest
The decision is straightforward: use zip when you want to stop at the shortest iterable, and use zip_longest when you need to preserve all data and explicitly handle missing values. The table below summarizes the difference:
| Behavior | zip | zip_longest |
|---|---|---|
| Stops when | shortest iterable ends | longest iterable ends |
| Missing values | not produced | filled with fillvalue |
| Infinite iterable | safe (stops) | unsafe (runs forever) |
| Common use | pairing equal-length sequences | padding, alignment, ragged data |
There is no performance reason to prefer one over the other unless you are in a tight loop with many iterables. In that case, benchmark your specific use case, but do not assume zip is always faster; the difference is usually small.
For production code, consider readability. If your data is expected to be of equal length, zip communicates that intent. If missing values are a real possibility, zip_longest makes that explicit and forces you to decide what the fill value should be. That clarity often prevents subtle bugs caused by silently dropped data.