Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›itertools — chain, product, combinations

Standard Library

itertools — chain, product, combinations

The itertools module provides highly optimized, memory-efficient building blocks for creating complex iterators from simpler ones. By leveraging these functions, you can write concise, performant code that avoids the overhead of manually managing nested loops or intermediate lists. You should reach for these tools whenever you need to process large data streams, perform combinatorial searches, or flatten multi-stage sequential data pipelines.

Flattening sequences with chain

The chain function is the most fundamental tool for treating multiple iterables as a single, contiguous sequence. When you pass multiple iterables to chain, it returns an iterator that yields elements from the first iterable until it is exhausted, then proceeds to the second, and so on. This mechanism works by creating a single unified iterator that delegates its next() calls to the current underlying iterable. The primary advantage of chain is that it performs this process lazily, meaning it never copies the elements of the input collections into a new list or memory structure. This is critical for performance when dealing with large datasets, as it maintains a constant memory footprint regardless of the size or number of the input iterables. Understanding chain requires recognizing that it essentially hides the complexity of managing loop switches between multiple source collections, allowing you to treat them uniformly as one stream of data during iteration.

from itertools import chain

# Combine different collection types into one sequence
letters = ['a', 'b', 'c']
numbers = (1, 2, 3)

# chain accepts any number of iterables
for item in chain(letters, numbers):
    print(item, end=' ') # Output: a b c 1 2 3

Generating permutations with product

The product function is the iterator equivalent of a nested loop structure. If you have ever written three or four nested for-loops to test every possible combination of variables from multiple lists, you have manually performed a Cartesian product. By using product, you abstract this logic into a single line of code that is both more readable and more efficient in execution. The function yields tuples representing every possible combination, taking one element from each input iterable in order. The internal mechanism functions like a odometer: the last iterable cycles the fastest, while the first iterable changes the slowest. This is incredibly powerful for scenarios such as generating test configurations, exhaustive searching of solution spaces, or building multi-dimensional coordinate grids. Because it is implemented in highly optimized C code within the standard library, it often outperforms equivalent manual Python loop implementations significantly while keeping memory usage predictably low by yielding results one at a time.

from itertools import product

# Generate all possible coordinate pairs from two lists
x_coords = [1, 2]
y_coords = [10, 20]

# This replaces a nested loop structure
for x, y in product(x_coords, y_coords):
    print(f"Coordinate: ({x}, {y})")

Calculating combinations without replacement

The combinations function is used when you need to select a fixed number of items from an input iterable where the order of selection does not matter and items cannot be reused. Unlike product, which considers every possible pairing, combinations enforces uniqueness based on the index of the elements in the input sequence. The reasoning behind its operation is based on lexicographic ordering; it generates tuples in the order they would appear if you were selecting items from a sorted input. This is vital for tasks like calculating potential team groupings from a larger pool of candidates or determining every possible two-item pairing within a set of data points. Because it avoids redundant pairs like (A, B) and (B, A), it significantly reduces the total number of operations compared to permutations. This makes it an essential tool for statistical sampling, game logic, and any algorithmic requirement involving selection from a group without replacement.

from itertools import combinations

# Select groups of 2 from a team of 4
players = ['Alice', 'Bob', 'Charlie', 'David']

# Order does not matter, and players aren't paired with themselves
for pair in combinations(players, 2):
    print(pair) # Output: ('Alice', 'Bob'), ('Alice', 'Charlie'), etc.

Working with combinations_with_replacement

While standard combinations restrict you to unique indices, combinations_with_replacement allows an element to be selected multiple times, which is a common requirement in discrete mathematics and optimization problems. This function provides a mechanism for selecting items where repetition is allowed but the final sequence order remains irrelevant. By allowing an element to effectively 'stay' in the pool after it is chosen, the iterator expands the total space of possible outcomes significantly. It is useful when you are assigning items to bins or calculating outcomes where you might choose the same input multiple times, such as selecting coins from a limited set of denominations to reach a target sum. Understanding this distinction is key: if the input is [1, 2], standard combinations gives (1, 2), but combinations_with_replacement yields (1, 1), (1, 2), and (2, 2). It provides the mathematical flexibility needed to model scenarios involving unlimited supply or interchangeable selection processes effectively.

from itertools import combinations_with_replacement

# Select 2 items from {1, 2}, allowing duplicates
for combo in combinations_with_replacement([1, 2], 2):
    print(combo) # Output: (1, 1), (1, 2), (2, 2)

Performance advantages of lazy evaluation

All functions within itertools share a common philosophy: lazy evaluation. Rather than materializing an entire result set into a list in RAM—which could trigger a memory error if the input space is large—these functions produce elements on-the-fly. This is a critical architectural decision for modern software. When you chain several iterators together, you are effectively constructing a processing pipeline where data flows through the logic only when requested by the consumer (such as a for-loop or a list comprehension). This 'pull-based' model ensures that even if you are calculating millions of combinations, your program only stores the current tuple in memory. Mastering this concept is what separates a novice developer from an expert. By moving away from eager list construction, you enable your applications to scale gracefully, handling inputs that would otherwise be impossible to process due to memory constraints or unnecessary computation overhead during program initialization.

from itertools import product, chain

# Chain two large product iterators lazily
large_grid1 = product(range(1000), range(1000))
large_grid2 = product(range(500), range(500))

# No memory allocation for the full product sets occurs here
combined = chain(large_grid1, large_grid2)
print(next(combined)) # Only the first item is calculated

Key points

  • The chain function allows multiple iterables to be treated as a single, seamless sequence.
  • The product function implements Cartesian products efficiently using a lazy iterator.
  • Combinations generate tuples representing unique selections where order does not matter.
  • Lazy evaluation is the core philosophy that ensures these functions remain memory-efficient.
  • The product function follows an odometer pattern where the final element cycles most frequently.
  • Combinations with replacement allow the same element to be chosen more than once per result.
  • Using these tools avoids the memory overhead associated with creating large, materialized intermediate lists.
  • These functions improve code maintainability by replacing deeply nested loop structures with declarative iterators.

Common mistakes

  • Mistake: Expecting itertools functions to return a list. Why it's wrong: They return iterators that consume memory only when iterated. Fix: Use list() to convert the result if you need index access or to view all elements at once.
  • Mistake: Using product() without understanding it calculates Cartesian products. Why it's wrong: Users often expect it to perform element-wise multiplication. Fix: Use a list comprehension with zip() or a numpy array for element-wise operations.
  • Mistake: Passing a single iterable to chain() instead of multiple iterables. Why it's wrong: chain() expects multiple iterables as separate arguments. Fix: Use chain.from_iterable() if you have a single iterable containing multiple iterables (like a list of lists).
  • Mistake: Assuming combinations() keeps the original order of input elements. Why it's wrong: combinations() is based on the input position, but it doesn't account for repeating elements unless combinations_with_replacement() is used. Fix: Ensure input is sorted if you need sorted combinations, or use the correct variant for your logic.
  • Mistake: Iterating over an itertools object twice. Why it's wrong: Iterators are exhausted after one pass. Fix: Convert the iterator to a list if you need to reuse the data, or recreate the iterator.

Interview questions

What is the purpose of itertools.chain, and when should you use it?

The itertools.chain function is used to treat multiple sequences as a single unified sequence without having to concatenate them into a new list. This is highly efficient for memory management because it returns an iterator that yields elements one by one. You should use chain when you want to iterate over several collections, such as lists or tuples, sequentially, as it avoids creating a temporary intermediate list in memory, which is beneficial when dealing with large datasets.

How does itertools.product differ from nested for-loops?

The itertools.product function generates the Cartesian product of input iterables, effectively replacing nested for-loops. While nested loops are readable for two or three levels, they become cumbersome and hard to maintain as nesting increases. By using product, you can pass an arbitrary number of iterables, making the code cleaner and more performant. It returns an iterator of tuples, which is the exact same structure as the output generated by the equivalent nested loops, but it is much more Pythonic and scalable.

Explain the difference between combinations and permutations using itertools.

The primary difference lies in the significance of order. itertools.combinations generates all possible subsets of a specified length where the order of elements does not matter, meaning (A, B) is treated the same as (B, A). Conversely, permutations considers the order to be significant, so (A, B) and (B, A) are treated as two distinct outputs. Use combinations when you are picking a set of items, and use permutations when the specific sequence of those items is vital.

Compare using itertools.product versus a list comprehension for generating combinations of elements.

When comparing itertools.product to a list comprehension, the main difference is memory usage and evaluation style. A list comprehension eagerly constructs the entire resulting list in memory, which can lead to performance degradation or errors if the input ranges are massive. In contrast, itertools.product returns a lazy iterator that computes elements on the fly. Therefore, product is the superior choice for memory efficiency, while a list comprehension is only suitable when you know for certain that the total number of combinations is small enough to fit within your system's available RAM.

In what scenario would you choose itertools.chain.from_iterable over a standard chain?

The standard itertools.chain function requires you to pass individual iterables as separate arguments, such as chain(list1, list2, list3). This becomes problematic if you have a single collection containing many iterables, such as a list of lists. In this scenario, itertools.chain.from_iterable is the preferred method because it accepts a single iterable of iterables as its input. It is more flexible because it can handle nested structures dynamically, whereas chain would require unpacking, which is awkward if the number of sub-iterables is unknown at runtime.

How can you leverage itertools to optimize memory usage when processing large data structures?

You can optimize memory by shifting from eager execution—where you generate entire lists—to lazy evaluation using itertools. For instance, instead of storing every possible combination of a list in memory, you can iterate over the combinations object directly. By treating these functions as generators, you consume one element at a time, allowing you to process datasets that are technically larger than your machine's physical memory. This approach keeps your script's memory footprint constant, regardless of the size of the input, as long as you process the returned iterator elements one by one.

All Python interview questions →

Check yourself

1. What is the primary benefit of using itertools.chain() over using the + operator to join three large lists?

  • A.It performs the operation faster because it uses optimized C code.
  • B.It avoids creating a new intermediate list in memory by lazily yielding elements.
  • C.It automatically sorts the resulting elements while joining them.
  • D.It removes duplicate elements during the chaining process.
Show answer

B. It avoids creating a new intermediate list in memory by lazily yielding elements.
chain() returns an iterator that yields elements one by one, whereas + creates a brand new list in memory containing all elements from the inputs. The other options are incorrect because chain does not sort, does not deduplicate, and memory efficiency is its specific design goal, not just raw execution speed.

2. You have a list of lists: data = [[1, 2], [3, 4]]. Which approach correctly yields all individual integers using itertools?

  • A.itertools.chain(data)
  • B.itertools.chain(*data)
  • C.itertools.product(data)
  • D.itertools.combinations(data, 2)
Show answer

B. itertools.chain(*data)
Using the unpacking operator (*) passes the sub-lists as separate arguments to chain(), which is the correct way to link them. chain(data) would treat the outer list as one object and fail to unpack the inner lists. product and combinations perform different mathematical operations entirely.

3. If you use itertools.product('AB', repeat=2), what is the resulting output sequence?

  • A.('A', 'A'), ('B', 'B')
  • B.('A', 'A'), ('A', 'B'), ('B', 'A'), ('B', 'B')
  • C.('A', 'B'), ('B', 'A')
  • D.('A', 'A'), ('A', 'B'), ('B', 'B')
Show answer

B. ('A', 'A'), ('A', 'B'), ('B', 'A'), ('B', 'B')
product with repeat=2 generates the Cartesian product of the set with itself. This includes all combinations of the items, including self-pairs. Option 1 is combinations, option 3 is permutations, and option 4 is incomplete.

4. How does itertools.combinations('ABC', 2) behave compared to itertools.permutations('ABC', 2)?

  • A.Combinations include ('A', 'B') and ('B', 'A'), while permutations only include ('A', 'B').
  • B.Combinations are sorted by index; permutations are not.
  • C.Combinations treats ('A', 'B') and ('B', 'A') as the same pair, while permutations treats them as distinct.
  • D.Combinations creates tuples, while permutations creates lists.
Show answer

C. Combinations treats ('A', 'B') and ('B', 'A') as the same pair, while permutations treats them as distinct.
Combinations represent unordered subsets, meaning order does not matter (A,B is the same as B,A). Permutations represent ordered arrangements, where order is significant. The other options misstate the fundamental mathematical definitions of these functions.

5. Why might you prefer itertools.chain.from_iterable(list_of_lists) over itertools.chain(*list_of_lists)?

  • A.It is faster because it does not require unpacking the entire list into function arguments.
  • B.It allows for the inclusion of non-iterable objects in the list.
  • C.It automatically removes empty iterables from the input.
  • D.It creates a list object instead of an iterator, preventing exhaustion.
Show answer

A. It is faster because it does not require unpacking the entire list into function arguments.
from_iterable is memory-efficient because it avoids the overhead of unpacking a potentially massive list into function arguments via the * operator. None of the other options are true: it does not handle non-iterables, it does not filter empty items, and it still returns an iterator.

Take the full Python quiz →

← Previouscollections — Counter, defaultdict, deque, namedtupleNext →json — Serialization and Deserialization

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app