How does itertools.chain() work and when would you use it?
itertools.chain(*iterables) takes multiple iterables and returns a single iterator that yields elements from each iterable in sequence, exhausting one before moving to the next. It does not create a new list in memory -- it lazily yields elements, making it highly memory-efficient for concatenating large sequences.
There is also itertools.chain.from_iterable(iterable) which takes a single iterable of iterables -- useful when you have a list of lists or a generator of generators.
Use cases:
- Concatenating multiple lists/generators without copying
- Flattening one level of nested iterables
- Processing files from multiple directories sequentially
chain(A, B, C):
for iterable in [A, B, C]:
for element in iterable:
yield element
chain.from_iterable([[A, B], [C, D]]):
for iterable in [[A, B], [C, D]]:
for element in iterable:
yield element
from itertools import chain
# Basic chain: concatenate multiple iterables
result = list(chain([1, 2], [3, 4], [5, 6]))
print(result) # [1, 2, 3, 4, 5, 6]
# chain.from_iterable: flatten one level of nesting
nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = list(chain.from_iterable(nested))
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Combining different iterable types
from_list = [1, 2, 3]
from_tuple = (4, 5, 6)
from_set = {7, 8, 9}
result = list(chain(from_list, from_tuple, from_set))
print(result) # [1, 2, 3, 4, 5, 6, 7, 8, 9] (set order may vary)
# Memory-efficient: works with generators
def gen_a():
yield from range(3)
def gen_b():
yield from range(3, 6)
for val in chain(gen_a(), gen_b()):
print(val, end=' ') # 0 1 2 3 4 5
- •chain() lazily yields elements -- no intermediate list is created
- •chain.from_iterable() accepts a single iterable of iterables
- •Works with any iterable type (lists, tuples, generators, sets)
- •Only flattens one level of nesting
- •chain() does NOT deep-flatten nested structures; [[1, [2, 3]], [4]] becomes [1, [2, 3], 4]
- •If you pass a string to chain(), each character is yielded individually since strings are iterable
- •chain.from_iterable() consumes the outer iterable lazily, but each inner iterable is consumed eagerly