Back to Blog
Python

Python itertools.chain: Combine Iterables Lazily

Learn how Python's itertools.chain combines multiple iterables into one lazy sequence without intermediate copies, saving memory in pipelines.

itertoolsPythoniteratorslazy evaluationgenerators
Illustration of itertools.chain combining three separate iterable streams into a single sequential pipeline.

itertools.chain() combines multiple iterables into one lazy sequence. It returns an iterator that yields elements from the first iterable until it is exhausted, then moves on to the next, without creating an intermediate list.

from itertools import chain combined = chain([1, 2, 3], [4, 5, 6]) for value in combined: print(value)

What itertools.chain Actually Does

itertools.chain(*iterables) returns an iterator that produces elements from the first iterable until it is exhausted, then moves to the next. It does not copy elements into a new container. Instead, it holds iterators obtained from the input iterables and pulls from them one at a time.

from itertools import chain list_a = [1, 2, 3] list_b = [4, 5, 6] combined = chain(list_a, list_b) print(list(combined)) # [1, 2, 3, 4, 5, 6]

The result is an iterator, not a list. If you need a list, you must convert it explicitly with list(). This distinction matters because iterators are single-pass: once you consume an element, it is gone.

Lazy Evaluation and Memory Behavior

A common reason to use chain() over list concatenation is memory. When you write list_a + list_b, Python allocates a new list and copies every element from both inputs. For large sequences, that is a full copy of the data.

chain() avoids that copy. It produces elements on demand, so the additional memory used by the returned iterator stays roughly constant even when the inputs are large. It does not copy the source iterables' contents. This is especially valuable when the inputs are themselves lazy, such as generator expressions or file handles.

from itertools import chain def read_lines(filename): with open(filename) as f: yield from f all_lines = chain(read_lines("a.txt"), read_lines("b.txt"))

Here, no file content is loaded into memory at once. Lines are yielded one at a time from the first file, then the second.

chain.from_iterable for Nested Iterables

When the iterables you want to combine are themselves inside a list or tuple, chain(*iterables) requires unpacking. That is fine for a few fixed iterables, but it can be awkward when the outer collection is large or generated dynamically.

from itertools import chain groups = [[1, 2], [3, 4], [5, 6]] # Unpacking works, but passes each group as a separate argument combined = chain(*groups) # from_iterable takes the outer iterable directly combined = chain.from_iterable(groups)

chain.from_iterable() takes a single iterable of iterables and flattens one level. This is the idiomatic way to flatten a list of lists, and it works with any iterable of iterables, including generator expressions.

from itertools import chain matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flat = list(chain.from_iterable(matrix)) # [1, 2, 3, 4, 5, 6, 7, 8, 9]

The difference matters when the outer iterable is itself a generator. With chain(*gen), Python must consume the entire generator to unpack it into positional arguments. With chain.from_iterable(gen), the outer iterable is consumed lazily.

Common Usage Patterns

Flattening a List of Lists

from itertools import chain rows = [[1, 2], [3, 4], [5, 6]] flat = list(chain.from_iterable(rows))

This is a standard replacement for a nested list comprehension like [x for row in rows for x in row]. The comprehension is often more readable for simple cases; chain.from_iterable is useful when you want to avoid nested syntax or when the outer iterable is itself a generator.

Combining Generator Outputs

from itertools import chain def even_numbers(limit): for n in range(limit): if n % 2 == 0: yield n def odd_numbers(limit): for n in range(limit): if n % 2 != 0: yield n combined = chain(even_numbers(10), odd_numbers(10))

Chaining File Contents

from itertools import chain with open("part1.txt") as f1, open("part2.txt") as f2: for line in chain(f1, f2): process(line)

This pattern treats multiple files as a single stream, which is handy in log processing and data pipelines.

Performance Considerations

The main performance advantage of chain() comes from avoiding intermediate allocations. List concatenation a + b creates a new list and copies all elements. chain() creates a single iterator object with minimal overhead.

For small lists, the difference is negligible. For large lists or many iterables, chain() avoids repeated copying. The time complexity of iterating over chain(a, b) is the same as iterating over a then b directly.

One thing to note: chain() does not sort or deduplicate. If you need unique elements, apply set() or another deduplication step afterward.

from itertools import chain combined = chain([1, 2, 2], [2, 3]) print(list(set(combined))) # [1, 2, 3]

When Not to Use chain()

chain() is not always the right tool. If you need random access to the combined sequence, an iterator will not help — you need a list or another sequence type. If you need to know the length upfront, an iterator does not provide that.

combined = chain([1, 2, 3], [4, 5]) print(len(combined)) # TypeError: object of type 'itertools.chain' has no len()

You must convert to a list first if you need len() or indexing.

Also, every argument to chain() must be iterable. Passing a non-iterable raises a TypeError at the call, before any values are produced.

Edge Cases and Behavior Details

  • chain() with no arguments returns an empty iterator.
  • chain() with a single iterable behaves like iter() on that iterable.
  • Elements are yielded in order: all from the first iterable, then the second, and so on.
  • If an input iterable is a generator, it is consumed lazily as chain() pulls from it.
from itertools import chain print(list(chain())) # [] print(list(chain([1, 2]))) # [1, 2]

Compatibility and Version Notes

itertools.chain has been part of the standard library since Python 2.3, and chain.from_iterable was added in Python 2.6. The examples in this article work in current Python 3 releases without modification.

Python itertools.chain: Usage and Code Examples | RYUSLOG DEV