itertools
The itertools module is part of Python's standard library and provides a set of fast, memory-efficient "building block" functions for working with iterators. Instead of building intermediate lists in memory, these tools produce values lazily, one at a time, which makes them ideal for looping over large — or even infinite — sequences of data. Once you're comfortable with the basics of iteration, itertools becomes one of the most useful modules for writing clean, efficient Python.
In this page we'll cover infinite iterators (count, cycle, repeat), combining iterables with chain, combinatorics with combinations and permutations, grouping consecutive items with groupby, and finish with a worked example that combines several of these tools together.
Infinite Iterators: `count()`, `cycle()`, `repeat()`
Three functions in itertools produce values forever, without ever raising StopIteration on their own:
count(start=0, step=1)— counts up (or down) indefinitely, like an infiniterange().cycle(iterable)— repeats the items of an iterable over and over, forever.repeat(value, times=None)— yields the same value repeatedly, forever unlesstimesis given.
from itertools import count
# DANGER: this loop never ends!
for n in count(1):
print(n)The safe way to take a limited number of values from an infinite iterator is itertools.islice(), which works like slicing but for any iterator:
from itertools import count, islice # Take only the first 5 values — this terminates safely. first_five = list(islice(count(1), 5)) print(first_five) # count() also supports a step, just like range() evens = list(islice(count(0, 2), 5)) print(evens)
[1, 2, 3, 4, 5] [0, 2, 4, 6, 8]
cycle() is handy for round-robin style logic — for example assigning tasks to a fixed set of workers in rotation:
from itertools import cycle, islice
workers = cycle(["Alice", "Bob", "Carol"])
tasks = ["task-1", "task-2", "task-3", "task-4", "task-5"]
for task, worker in zip(tasks, workers):
print(f"{task} -> {worker}")task-1 -> Alice task-2 -> Bob task-3 -> Carol task-4 -> Alice task-5 -> Bob
Notice that zip() here is what keeps things safe — it stops once the shorter iterable (tasks) is exhausted, even though cycle(workers) would otherwise run forever. repeat() is often used to supply a constant argument to functions like map():
from itertools import repeat squares_of_five = list(map(pow, repeat(5, 3), [1, 2, 3])) print(squares_of_five) # 5**1, 5**2, 5**3
[5, 25, 125]
`chain()`: Combining Iterables Without Copying
itertools.chain() takes several iterables and walks through them one after another as if they were a single sequence — without ever building a new combined list in memory. This is more efficient than writing list1 + list2 when you just need to iterate, especially for large sequences or generators.
from itertools import chain
fruits = ["apple", "banana"]
vegetables = ["carrot", "potato"]
grains = ("rice", "oats")
for item in chain(fruits, vegetables, grains):
print(item)apple banana carrot potato rice oats
`combinations()` vs `permutations()`
Both functions pick groups of items from an iterable, but they answer different questions:
combinations(iterable, r)— all ways to chooseritems where order does not matter.(A, B)and(B, A)count as the same combination, so only one is produced.permutations(iterable, r)— all ways to chooseritems where order matters.(A, B)and(B, A)are counted separately.
Here's the difference made concrete with three letters, picking 2 at a time:
from itertools import combinations, permutations
letters = ["A", "B", "C"]
print("combinations:", list(combinations(letters, 2)))
print("permutations:", list(permutations(letters, 2)))combinations: [('A', 'B'), ('A', 'C'), ('B', 'C')]
permutations: [('A', 'B'), ('B', 'A'), ('A', 'C'), ('C', 'A'), ('B', 'C'), ('C', 'B')]combinations() produced 3 pairs; permutations() produced 6, because it counts (A, B) and (B, A) as different results. A good rule of thumb: if swapping the order of two chosen items gives you a genuinely different outcome (like first place vs. second place in a race), use permutations(). If it doesn't (like two people paired up to work together), use combinations().
`groupby()`: Grouping Consecutive Items
itertools.groupby() groups consecutive items in an iterable that share the same key. This is a common source of bugs: groupby() does not scan the whole sequence looking for matches — it only groups items that are already next to each other.
from itertools import groupby
# NOT sorted by first letter - "avocado" and "apple" are separated by "banana"
words = ["apple", "banana", "avocado", "cherry"]
for key, group in groupby(words, key=lambda w: w[0]):
print(key, list(group))a ['apple'] b ['banana'] a ['avocado'] c ['cherry']
Notice "a" appears twice — once for "apple" and again for "avocado" — because they weren't adjacent. Sorting first fixes this:
from itertools import groupby
words = ["apple", "banana", "avocado", "cherry"]
words_sorted = sorted(words, key=lambda w: w[0])
for key, group in groupby(words_sorted, key=lambda w: w[0]):
print(key, list(group))a ['apple', 'avocado'] b ['banana'] c ['cherry']
Worked Example: Tournament Pairings
Let's combine a couple of these tools in a small, realistic scenario. Suppose we're running a round-robin chess tournament and need every unique pair of players who will face each other, split by which skill division they're in. We'll use groupby() to split players into divisions, and combinations() to generate the unique pairs within each division.
from itertools import combinations, groupby
players = [
("Ada", "Advanced"),
("Liu", "Advanced"),
("Sam", "Advanced"),
("Ravi", "Beginner"),
("Mia", "Beginner"),
]
# Must be sorted by division first for groupby() to work correctly.
players_sorted = sorted(players, key=lambda p: p[1])
for division, group in groupby(players_sorted, key=lambda p: p[1]):
names = [name for name, _ in group]
pairs = list(combinations(names, 2))
print(f"{division} division ({len(names)} players, {len(pairs)} matches):")
for pair in pairs:
print(f" {pair[0]} vs {pair[1]}")Advanced division (3 players, 3 matches): Ada vs Liu Ada vs Sam Liu vs Sam Beginner division (2 players, 1 matches): Ravi vs Mia
This is exactly the kind of task itertools shines at: no manual nested loops, no intermediate lists of duplicated pairs to filter — just composing small, well-tested pieces together.
Quick Reference
Function | Purpose | Stops on its own? |
|---|---|---|
| Infinite counting sequence | No — use |
| Repeats an iterable forever | No — use |
| Repeats a value (optionally n times) | Only if |
| Walks multiple iterables as one sequence | Yes, when all are exhausted |
| Unordered selections of size | Yes |
| Ordered selections of size | Yes |
| Groups consecutive items sharing a key | Yes |
itertoolsfunctions return lazy iterators, not lists — wrap inlist(...)when you need to see or store all the results.Always bound infinite iterators (
count,cycle,repeatwithouttimes) withislice(),zip()against a finite iterable, or an explicitbreak.groupby()only groups adjacent matching items — sort your data by the grouping key first.