Python Module 10 – Iterators, generators and functional tools
itertools: the iteration toolkit
Slice, chain, group, batch and pair iterators with itertools, generate combinations and permutations, and estimate how fast those search spaces grow.
What you will learn
- Pick the itertools function for a job, from islice and chain to groupby and batched
- Group sorted records with groupby and process them in batches with batched
- Generate combinations, permutations and products, and estimate their size first
- Avoid the traps of groupby, tee and accumulate
- Limit a search space by pruning partial results instead of filtering every result
Before you start
What you’ll learn · Before you start
What you will learn
- Pick the itertools function for a job, from islice and chain to groupby and batched
- Group sorted records with groupby and process them in batches with batched
- Generate combinations, permutations and products, and estimate their size first
- Avoid the traps of groupby, tee and accumulate
- Limit a search space by pruning partial results instead of filtering every result
Before you start
On this page
Cutting 120 message recipients into requests of 50, pairing five meter readings with their neighbours and listing
the 15 pairs a team of six can form are three loops, and each one is a single call from itertools. The module
holds the standard library’s iterator building blocks: every function in it returns
Said of an iterator that makes each item only when a loop or a call asks for the next one, so nothing is computed ahead and an endless one costs nothing until it is read.
iterator: An object that hands out items one at a time: next() returns its next item, or raises StopIteration when none are left. It keeps one position, so it is used up after one pass, and iter() returns the iterator itself. More in “How iteration works: iterables and iterators”, so its tools combine like generator stages and work on inputs of any size.
Below you will group attendance records, batch messages and count pairings before generating them, stepping through
real runs of groupby() and pairwise().
Every itertools function returns a lazy iterator
count(1001) can number orders for ever, because it makes each number only when something asks for the next one,
and islice() takes as many as you need. Every tool of the module works this way. They fall into four families:
endless sources, tools that slice or join, tools that group, and combinatorics.
| Function | What it gives |
|---|---|
count(start, step) |
Numbers for ever |
cycle(it) |
The items of it again and again |
repeat(x, n) |
The same value, n times or for ever |
islice(it, start, stop, step) |
A slice of any iterator |
chain(a, b), chain.from_iterable(xs) |
One iterable after another |
takewhile(f, it), dropwhile(f, it) |
The front while a test holds, or what follows it |
compress(it, flags) |
The items whose flag is true |
groupby(it, key) |
Each run of equal keys, with an iterator over it |
batched(it, n) |
Tuples of n items; the last may be shorter |
pairwise(it) |
Overlapping pairs of neighbours |
accumulate(it, f, initial=) |
Running totals, or running results of any two-argument function |
tee(it, n) |
n independent iterators over one input |
product, permutations, combinations, combinations_with_replacement |
Every tuple of a search space |
Version note
pairwise() arrived in Python 3.10, batched() in 3.12 and its strict=True option, which raises ValueError
for a short last batch, in 3.13.
The program below runs one call of each tool, in the order of the table. Press Edit and change any call:
from itertools import (accumulate, batched, chain, combinations, compress, count, cycle,
groupby, islice, pairwise, repeat, takewhile, tee)
def show(name, items):
"""Reads an iterator to the end and prints what it gave, tuples as joined letters."""
words = ["".join(map(str, x)) if isinstance(x, tuple) else str(x) for x in items]
print(f"{name:13}", " ".join(words))
show("count", islice(count(5), 3)) # endless: islice() takes the first 3
show("cycle", islice(cycle("ab"), 4)) # endless too
show("repeat", repeat(0, 3))
show("islice", islice("abcdef", 1, 4))
show("chain", chain("ab", "cd"))
show("takewhile", takewhile(lambda x: x < 3, [1, 2, 5, 1]))
show("compress", compress("abc", [1, 0, 1]))
show("groupby", (key for key, run in groupby("aabca")))
show("batched", batched("abcde", 2))
show("pairwise", pairwise("abc"))
show("accumulate", accumulate([1, 2, 3]))
first, second = tee("ab")
show("tee", chain(first, "|", second))
show("combinations", combinations("abc", 2)) Output
count 5 6 7 cycle a b a b repeat 0 0 0 islice b c d chain a b c d takewhile 1 2 compress a c groupby a b c a batched ab cd e pairwise ab bc accumulate 1 3 6 tee a b | a b combinations ab ac bc
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 toolkit.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The two endless tools get a limit from islice(), and chain() read "ab" to its end before it started on "cd",
without building a new list. zip_longest(), starmap() and filterfalse() complete the module.
Key idea
An itertools tool makes nothing until a loop or a call such as list() asks for an item, so an endless one needs
islice() or a break.
Checkpoint
Check: Lazy iterators of itertools
groupby() groups runs of equal keys, so sort first
Six attendance records arrive in the order they were marked, Monday and Tuesday in turn, and the report needs the names present on each day:
from itertools import groupby
from operator import itemgetter
records = [
("Mon", "Asha", "present"),
("Tue", "Ravi", "absent"),
("Mon", "Ravi", "present"),
("Tue", "Asha", "present"),
("Mon", "Meera", "absent"),
("Tue", "Meera", "present"),
]
day = itemgetter(0)
print("unsorted:", [(d, len(list(g))) for d, g in groupby(records, key=day)])
records.sort(key=day) # equal days next to each other
for d, group in groupby(records, key=day):
present = [name for _, name, status in group if status == "present"]
print(d, "present:", ", ".join(present)) Output
unsorted: [('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1)]
Mon present: Asha, Ravi
Tue present: Asha, Meera
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 attendance.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
groupby() groups runs of equal keys, not equal keys
Text description of the diagram
The diagram has two columns of the six attendance records, read from top to bottom.
- Left, as recorded: Mon Asha, Tue Ravi, Mon Ravi, Tue Asha, Mon Meera, Tue Meera. The day changes at every record, so groupby() starts a new group each time and yields six groups of one record.
- Right, sorted by day first: Mon Asha, Mon Ravi, Mon Meera, then Tue Ravi, Tue Asha, Tue Meera. Equal days are now next to each other, so groupby() yields two groups, Mon with three records and Tue with three. The sort keeps the records of each day in the order they arrived.
On the records as they arrived, the first line shows six groups of one, the left column of the diagram.
groupby() works like the Unix uniq command: it starts a new run: A stretch of neighbouring items that share a key, such as three Mon records in a row. itertools.groupby() yields one group per run, so a key whose items are apart forms several runs. every time the key changes, so it groups
neighbours, not equal keys. Step through the same six days, as they arrived and sorted. The flag under days is
the position groupby() reads next:
Program · day_runs.py
from itertools import groupby
days = ["Mon", "Tue", "Mon",
"Tue", "Mon", "Tue"]
runs = []
for day, run in groupby(days):
runs.append((day, len(list(run))))
print(runs)
days.sort()
runs = []
for day, run in groupby(days):
runs.append((day, len(list(run))))
print(runs) Variables
- changed in this step
- passed by the loop
- read next
Printed so far
Nothing printed yet[('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1)] [('Mon', 3), ('Tue', 3)]
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 26 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
from itertools import groupbyloadsgroupby. - Line 3.
days = ["Mon", "Tue", "Mon", "Tue", "Mon", "Tue"]makesdaysa list of 6 items. - Line 5.
runs = []makesrunsa list of 0 items. - Line 6.
for day, run in groupby(days):starts:dayis'Mon'andrunis<_grouper object>. groupby() has read 'Mon' at position 0 and hands it out as the key, with run, an iterator over that group. - Line 7.
runs.append((day, len(list(run))))adds('Mon', 1)to the end ofruns. Key point To find the end of the group, groupby() read 'Tue' at position 1. The key changed, so the Mon group holds one day, and 'Tue' starts the next group. - Line 6.
for day, run in groupby(days):goes round again:dayis'Tue'andrunis<_grouper object>. - Line 7.
runs.append((day, len(list(run))))adds('Tue', 1)to the end ofruns. - Line 6.
for day, run in groupby(days):goes round again:dayis'Mon'andrunis<_grouper object>. The bug Mon again, as a third group. groupby() compares each key only with the one before it, so the Mon days at positions 0 and 2 never meet. - Line 7.
runs.append((day, len(list(run))))adds('Mon', 1)to the end ofruns. - Line 6.
for day, run in groupby(days):goes round again:dayis'Tue'andrunis<_grouper object>. - Line 7.
runs.append((day, len(list(run))))adds('Tue', 1)to the end ofruns. - Line 6.
for day, run in groupby(days):goes round again:dayis'Mon'andrunis<_grouper object>. - Line 7.
runs.append((day, len(list(run))))adds('Mon', 1)to the end ofruns. - Line 6.
for day, run in groupby(days):goes round again:dayis'Tue'andrunis<_grouper object>. - Line 7.
runs.append((day, len(list(run))))adds('Tue', 1)to the end ofruns. - Line 6.
for day, run in groupby(days):has no item left, so the loop ends. - Line 8.
print(runs)prints[('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1)]. - Line 10.
days.sort()puts the items ofdaysin a new order:['Mon', 'Mon', 'Mon', 'Tue', 'Tue', 'Tue']. The fix Sorting with the same key puts the three Mon days side by side, so they form one run. - Line 11.
runs = []setsrunsto[](it was[('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1), ('Mon', 1), ('Tue', 1)]). - Line 12.
for day, run in groupby(days):starts:dayis'Mon'andrunis<_grouper object>. - Line 13.
runs.append((day, len(list(run))))adds('Mon', 3)to the end ofruns. This time the Mon group took positions 0 to 2. groupby() read 'Tue' at position 3 to see it end, so the group holds three days. - Line 12.
for day, run in groupby(days):goes round again:dayis'Tue'andrunis<_grouper object>. - Line 13.
runs.append((day, len(list(run))))adds('Tue', 3)to the end ofruns. - Line 12.
for day, run in groupby(days):has no item left, so the loop ends. - Line 14.
print(runs)prints[('Mon', 3), ('Tue', 3)]. That was the last line, so the program ends. Six groups as the days arrived, two once they were sorted. groupby() groups neighbours with equal keys, never equal keys that are apart.
To see where a group ends, groupby() reads one item past it, so a key that differs from the one before starts a
new group. Sorting by the same key put each day’s records side by side, in the order they came. A sort costs
O(n log n) and needs a list; data that already arrives in key order, such as a log sorted by time, needs neither.
Grouping records that are not sorted by the key
On records in arrival order, groupby(records, key=day) gave six groups for two days. Sort with the same key
first, records.sort(key=day).
Checkpoint
Check: groupby() and runs of keys
batched() cuts fixed-size groups and pairwise() pairs neighbours
An API that accepts at most 50 recipients per request needs a list of 120 cut into pieces, and a meter read once a day needs each reading paired with the next:
from itertools import batched, pairwise
recipients = [f"user{n:03d}" for n in range(120)]
sizes = [len(batch) for batch in batched(recipients, 50)]
print("messages in each request:", sizes)
meter = [1520, 1528, 1541, 1541, 1560] # electricity meter, one reading a day
used = [b - a for a, b in pairwise(meter)]
print("units used each day:", used) Output
messages in each request: [50, 50, 20] units used each day: [8, 13, 0, 19]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 batches_and_pairs.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
batched() cut the 120 recipients lazily into requests of 50, 50 and 20: each batch is a tuple of up to n items, and
the last holds whatever is left. pairwise() yields each item with the one after it, so a day’s use is one
subtraction per pair. Step through the meter loop and watch a, b and the flag under meter:
Program · meter_pairs.py
from itertools import pairwise
meter = [1520, 1528, 1541, 1541, 1560]
used = []
for a, b in pairwise(meter):
used.append(b - a)
print(used) Variables
- changed in this step
- passed by the loop
- read next
Printed so far
Nothing printed yet[8, 13, 0, 19]
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 14 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
from itertools import pairwiseloadspairwise. - Line 3.
meter = [1520, 1528, 1541, 1541, 1560]makesmetera list of 5 items. - Line 4.
used = []makesuseda list of 0 items. - Line 5.
for a, b in pairwise(meter):starts:ais1520andbis1528. pairwise() read positions 0 and 1 and gives them as one pair. The flag shows it reads position 2 next. - Line 6.
used.append(b - a)adds8to the end ofused. - Line 5.
for a, b in pairwise(meter):goes round again:ais1528andbis1541. Key point The new pair starts with 1528, the end of the last one. pairwise() reads each reading once and keeps only the last, to pair it with the next. - Line 6.
used.append(b - a)adds13to the end ofused. - Line 5.
for a, b in pairwise(meter):goes round again:ais1541andbis1541. - Line 6.
used.append(b - a)adds0to the end ofused. Two equal readings are a pair too, so the day's use is 0. - Line 5.
for a, b in pairwise(meter):goes round again:ais1541andbis1560. - Line 6.
used.append(b - a)adds19to the end ofused. - Line 5.
for a, b in pairwise(meter):has no item left, so the loop ends. - Line 7.
print(used)prints[8, 13, 0, 19]. That was the last line, so the program ends. Five readings gave four pairs, so used holds four days of use.
Five readings gave four pairs, and pairwise() read each reading once. Before Python 3.10 the usual way to pair
neighbours was zip(xs, xs[1:]), which needs a list; pairwise() works on any iterator, the lines of a file
included.
Checkpoint
Check: batched() and pairwise()
combinations() ignores order and permutations() keeps it: count before you loop
Six people can form 15 pairs for pair programming and fill the roles of captain and vice-captain in 30 ways:
import math
from itertools import combinations, permutations
team = ["Asha", "Ravi", "Meera", "Arjun", "Zoya", "Dev"]
pairs = list(combinations(team, 2)) # order does not matter
print(len(pairs), math.comb(6, 2), pairs[:3])
roles = list(permutations(team, 2)) # (captain, vice-captain): order matters
print(len(roles), math.perm(6, 2), roles[:3])
print("all orders of the whole team:", math.factorial(6)) Output
15 15 [('Asha', 'Ravi'), ('Asha', 'Meera'), ('Asha', 'Arjun')]
30 30 [('Asha', 'Ravi'), ('Asha', 'Meera'), ('Asha', 'Arjun')]
all orders of the whole team: 720
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 team_pairings.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
combinations(team, 2) yields each pair once, in the order of the input, and ignores order inside the pair: each
tuple is a combination: A choice of items in which order does not matter: (Asha, Ravi) and (Ravi, Asha) are one combination. itertools.combinations() generates them, and math.comb(n, k) counts them without generating any.. permutations(team, 2) treats (“Asha”, “Ravi”) and (“Ravi”,
“Asha”) as different permutation: An arrangement of items in which order matters: (Asha, Ravi) and (Ravi, Asha) are two permutations. itertools.permutations() generates them, and math.perm(n, k) counts them without generating any., which fits roles. math.comb() and math.perm()
give the same counts without generating anything, and that is the habit to build: count a
search space: Every candidate a brute-force search would try, such as the 720 orders of six people. Counting it before the loop, for example with math.comb() or math.perm(), shows whether the loop can finish. before you loop over it. The last line is a warning: six people have
720 orders, a factorial: n!, the product 1 × 2 × … × n: the number of orders of n different items. 4! is 24 and 6! is 720, and math.factorial(n) computes it., so every person added multiplies the count again. Pruning, which
drops a partial order the moment it breaks a rule, can skip most of them. The two generators side by side:
| Criterion | combinations(team, 2) | permutations(team, 2) |
|---|---|---|
| Order inside a tuple | ignored: each pair once | kept: both orders |
| Tuples for 6 people | 15 | 30 |
| Counted first with | math.comb(6, 2) | math.perm(6, 2) |
| When to choose | Pairs, teams, committees | Roles, rankings, routes |
Count with your real sizes
A test with six people runs in an instant even when the real input would take hours, so print math.comb() or
math.perm() of the real sizes before you write the loop.
Checkpoint
Check: Combinations, permutations and counts
Check yourself
Each question goes to the idea you have answered least well so far, and its answer is explained as soon as you choose.
Practice that adapts to you
Practise this lesson
13 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
groupby, tee and accumulate in depth, with the traps reproduced Pro: opens with a Pro, Premium or Ultimate pass
Three tools of the module keep state between items, and their state explains their traps. This part reproduces the
groups that vanish, measures the memory that tee() holds, and turns accumulate() into a tool for balances and
drawdowns.
In this part:
- groupby’s groups share one iterator: groups that vanish, reproduced
- tee buffers what one branch has read and the other has not, measured
- accumulate beyond running totals: balances, lows and the largest fall
- islice, takewhile, dropwhile and compress, compared
- Ragged data: flattening one level and filling the gaps
5 runnable examples (Python) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Search spaces with numbers: product, permutations and pruning Pro: opens with a Pro, Premium or Ultimate pass
Combinatoric iterators are lazy, but laziness does not shrink the space they walk through. This part puts numbers on
how fast each one grows, shows pruning cutting 3,628,800 orders to about 111,000 steps, builds a test matrix with
product() and ends with an exercise.
In this part:
- How fast each generator grows, with the hours worked out
- The four counting rules side by side, with their formulas
- Seat plans: 3,628,800 orders checked against 111,461 partial rows with pruning
- product for test matrices, and cutting the matrix down
- Exercise: attendance streaks and seat plans
- Attendance streaks and seat plans with itertools
1 diagram · 3 runnable examples (Python) · 1 exercise with sample tests · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Why do you usually sort the data before calling itertools.groupby()?
groupby() does not collect equal keys from the whole input. It walks through the iterable once and starts a new group every time the key changes, like the Unix uniq command, so it groups runs of neighbouring items with the same key. On unsorted data the same key appears in several separate groups: records for Mon, Tue and Mon give three groups, not two. Sorting by the same key function first puts equal keys next to each other, so each key forms one group. Sorting costs O(n log n) and needs the data in a list, so when the data already arrives in key order, such as a log sorted by time, skip the sort. When it cannot be sorted, group with a dictionary of lists or a Counter instead.
-
Mid to senior
A service splits one stream of events with itertools.tee() into two branches: one counts events per minute, the other writes them to an archive in batches whenever a timer fires. Memory climbs for hours until the process is killed. Explain why, and restructure the code.
-
Fresher to senior
A checkout tries every combination of the 30 coupons in a customer's wallet, using itertools.combinations for every size, to find the cheapest valid set. It passed its tests with 8 coupons and times out in production. Estimate the work, and redesign it.
-
Mid to senior
You must send 1.2 million notifications through an API that accepts at most 500 recipients per request and 20 requests a second. Write the batching loop with itertools, estimate the run time, and say what happens to the last batch and to a request that fails halfway.
Key takeaways
- Every
itertoolsfunction returns a lazy iterator, so the tools combine like generator stages and work on endless or huge inputs. groupby()groups runs of equal keys, so sort by the same key first unless the data already arrives in that order.batched()cuts tuples of up to n items andpairwise()yields overlapping neighbours, and both read their input once, lazily.combinations()ignores order andpermutations()keeps it; count a search space withmath.comb()ormath.perm()before you generate it.
Terms in this lesson
- combination
- A choice of items in which order does not matter: (Asha, Ravi) and (Ravi, Asha) are one combination. itertools.combinations() generates them, and math.comb(n, k) counts them without generating any.
- factorial
- n!, the product 1 × 2 × … × n: the number of orders of n different items. 4! is 24 and 6! is 720, and math.factorial(n) computes it.
- permutation
- An arrangement of items in which order matters: (Asha, Ravi) and (Ravi, Asha) are two permutations. itertools.permutations() generates them, and math.perm(n, k) counts them without generating any.
- run
- A stretch of neighbouring items that share a key, such as three Mon records in a row. itertools.groupby() yields one group per run, so a key whose items are apart forms several runs.
- search space
- Every candidate a brute-force search would try, such as the 720 orders of six people. Counting it before the loop, for example with math.comb() or math.perm(), shows whether the loop can finish.
References
- itertools: Functions creating iterators for efficient looping (Python Software Foundation)
- itertools.groupby() (Python Software Foundation)
- itertools.batched() (Python Software Foundation)
- itertools.pairwise() (Python Software Foundation)
- itertools.tee() (Python Software Foundation)
- itertools.accumulate() (Python Software Foundation)
- math.comb() and math.perm() (Python Software Foundation)
- Sorting Techniques: sort stability (Python Software Foundation)
- Functional Programming HOWTO: The itertools module (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress