itertools.chain() Silently Skips Flattening a List of Lists (tested)
itertools.chain() is the standard way to flatten several lists into one sequence. We measured what happens when you hand it a single variable that already holds a list of lists, instead of the lists themselves, and it quietly does nothing useful -- no error, just the wrong shape back.
1. chain() on a single list-of-lists argument does not flatten it
import itertools
lists = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain(lists)))
[[1, 2], [3, 4], [5, 6]]
Measured: the result is the same three sublists, completely unflattened -- not the six individual numbers someone merging paginated results would expect. chain() treats its single argument as one iterable to chain "with nothing else," so it just hands back what was already there. No exception is raised; the output simply has the wrong shape.
2. Unpacking with * or using chain.from_iterable() both flatten correctly
import itertools
lists = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain(*lists)))
print(list(itertools.chain.from_iterable(lists)))
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
Measured: both give the flattened result. chain(*lists) unpacks the outer list into separate positional arguments first; chain.from_iterable(lists) is built specifically to take one iterable of iterables without unpacking. They produce identical output -- from_iterable() is the better choice when the number of sublists is only known at runtime, since *lists works fine too but a huge number of sublists could in principle hit Python's argument-count limits where from_iterable() never would.
3. chain.from_iterable() only starts a sub-generator when it's actually needed
import itertools
started = []
def make_gen(label, n):
def gen():
started.append(label)
for i in range(n):
yield f"{label}{i}"
return gen()
sources = [make_gen("A", 3), make_gen("B", 3), make_gen("C", 3)]
chained = itertools.chain.from_iterable(sources)
result = list(itertools.islice(chained, 4)) # only need 4 items total
print("result:", result)
print("generators actually started:", started)
result: ['A0', 'A1', 'A2', 'B0']
generators actually started: ['A', 'B']
Measured: pulling only 4 items starts generator A and B, but generator C never runs at all -- confirmed by the started list only containing two entries. This is the opposite kind of surprise from the flattening bug above: it's good news, not a trap. If your sub-iterables are expensive (API calls, file reads, database queries), chain.from_iterable() won't pay that cost for sources it never ends up reading from.
Frequently asked questions
Why does itertools.chain() return my sublists unflattened?
We measured this: if you pass chain() a single variable that holds a list of lists, it treats that whole list as one iterable to chain -- there's nothing else to combine it with, so you get the sublists back exactly as they were, no error.
What's the fix for flattening a list of lists with itertools.chain?
Either unpack it with chain(*lists), or use chain.from_iterable(lists), which is built specifically to take one iterable of iterables. Both produced the identical flattened result when we measured them.
Does itertools.chain() raise an error when given the wrong shape of argument?
No -- we confirmed it fails silently. There's no exception, just an output shape that doesn't match what you probably wanted, which makes it easy to miss in a larger pipeline.
Does itertools.chain.from_iterable() run all the sub-iterables immediately?
No -- we measured this directly: it only starts each sub-iterable when it's actually reached. Pulling 4 items from 3 generators of 3 items each started the first two generators and never touched the third.
When should I use chain.from_iterable() instead of chain(*lists)?
Use from_iterable() whenever the number of sublists is only known at runtime, or when the sub-iterables are expensive and you want to avoid starting ones you'll never read from. chain(*lists) works fine for a small, fixed number of sublists known in advance.
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in itertools module: called itertools.chain() with a single list-of-lists argument, then compared it against itertools.chain(*lists) and itertools.chain.from_iterable(lists). Separately measured chain.from_iterable()'s laziness by tracking which of three generators actually started running. PlainHeaders is our extension; Python's itertools module is built in.