Back to Blog
Python

Python itertools chain: Combine Iterables Lazily

python itertools chain: Learn how itertools.chain combines multiple iterables into a single lazy sequence, avoiding intermediate copies and improving memory efficiency...

itertoolsPythoniteratorslazy evaluationgenerators
Illustration of itertools.chain combining three separate iterable streams into a single sequential pipeline.
from itertools import chain combined = chain([1, 2, 3], [4, 5, 6]) for value in combined: print(value)

This is the core of python itertools chain. The chain() function takes multiple iterables and returns an iterator that yields elements from the first, then the second, and so on, without creating an intermediate list.

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 references to 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

The main 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 entirely. It produces elements on demand, so the memory footprint stays constant regardless of how many elements the input iterables contain. 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 your iterables are themselves contained in a list or tuple, chain(*iterables) requires unpacking, which can be problematic if the number of iterables is large or dynamic.

from itertools import chain groups = [[1, 2], [3, 4], [5, 6]] # Unpacking works but creates a tuple of all groups combined = chain(*groups) # from_iterable avoids the unpacking 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 nested list comprehensions like [x for row in rows for x in row]. The comprehension is more readable for simple cases, but chain.from_iterable is faster because it avoids repeated extend calls.

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 is common in log processing and data pipelines where multiple files need to be treated as a single stream.

Performance Considerations

The 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 — there is no extra per-element cost beyond the iterator protocol.

One thing to note: chain() does not sort or deduplicate. If you need unique elements, you must 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, chain() only works with iterables. If you pass a non-iterable, it raises a TypeError when iteration begins, not at the call site.

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 is available in every Python 3 version. chain.from_iterable was added in Python 2.6. There are no version-specific behavioral differences in modern Python 3 releases, so code using chain() is portable across Python 3.x without modification.

python itertools chain: Practical Usage and Code Examples | RYUSLOG DEV