zip_longest Fillvalue None Looks Identical to Real None Data (tested)

itertools.zip_longest() fills in missing values from a shorter iterable with None by default. We measured what that output looks like when the data itself legitimately contains None, and the two cases are completely indistinguishable from the result alone.

1. A real None value in the data

import itertools

a = [1, 2, 3]
b = [10, None, 30]  # b really does contain None at index 1
print(list(itertools.zip_longest(a, b)))
[(1, 10), (2, None), (3, 30)]

Measured: the pair (2, None) at index 1 reflects a genuine data value -- b[1] really is None.

2. b simply ran out of items -- same output shape

import itertools

a = [1, 2, 3]
b = [10]  # b runs out after index 0
print(list(itertools.zip_longest(a, b)))
[(1, 10), (2, None), (3, None)]

Measured: the pair (2, None) shows up again at index 1 -- but this time it means "there was no second item at all," not "the second item is None." Looking at either tuple in isolation, (2, None) is byte-for-byte identical in both scenarios. There is no way to tell, from the output alone, whether you're looking at real data or the absence of data.

3. The ambiguity gets worse the more None values the real data has

import itertools

a = [1, 2, 3, 4]
b = [10, None, None]  # b has two REAL Nones, then runs out
print(list(itertools.zip_longest(a, b)))
[(1, 10), (2, None), (3, None), (4, None)]

Measured: indices 1 and 2 are real None values from b; index 3 is the fillvalue because b ran out. All three Nones print identically, and nothing downstream can tell which is which without separately knowing each list's original length.

4. Fix: pick a fillvalue that can't collide with real data, or check lengths yourself

import itertools

_MISSING = object()  # a sentinel that can never equal a real data value
a = [1, 2, 3]
b = [10]
for x, y in itertools.zip_longest(a, b, fillvalue=_MISSING):
    if y is _MISSING:
        print(x, "-> b ran out here")
    else:
        print(x, y)
1 10
2 -> b ran out here
3 -> b ran out here

Measured: passing a unique sentinel object as fillvalue instead of relying on the default None makes "ran out" and "real data" distinguishable again with a simple is check -- as long as the sentinel can never legitimately appear in your real data, which an object() instance guarantees.

Frequently asked questions

Does itertools.zip_longest tell me when an iterable ran out versus a real None value?

No -- we measured this: both cases produce exactly the fillvalue (None by default) in the output tuple, with no marker distinguishing 'this iterable was exhausted' from 'this data point really is None.'

How do I tell the difference between a real None and zip_longest's fillvalue?

Pass a unique sentinel object as fillvalue (for example fillvalue=object()) instead of relying on the default None, then check for that sentinel with an is comparison -- it can't collide with real data the way None can.

Is this a bug in itertools.zip_longest?

No -- it's documented, expected behavior: fillvalue is just a value substituted in, with no separate tracking of why it was used. The gotcha is relying on None specifically when your real data might also contain None.

Does this problem get worse with more missing values?

Yes -- we measured a case with two real None values followed by one fillvalue None; all three printed identically, so the more None values your real data has, the harder it gets to reason about which ones are genuine without a sentinel.

Does changing fillvalue to something other than None fully fix this?

Only if the chosen fillvalue can never appear as real data -- a fixed value like 0 or an empty string can still collide with legitimate data. A fresh object() instance is the safe choice because it's guaranteed unique.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in itertools module: ran itertools.zip_longest() on two lists in two separate scenarios -- one where the second list legitimately contains None at a given position, one where the second list is simply shorter and runs out at that same position -- and compared the output tuples. PlainHeaders is our extension; Python's itertools module is built in.