These are the three infinite iterators. count(start, step) yields an
endless arithmetic sequence. cycle(iterable) repeats an iterable's items
forever. repeat(value, times) yields the same value endlessly, or times
times if given.
from itertools import count, cycle, repeat, islice
list(islice(count(10, 2), 3)) # [10, 12, 14]
list(islice(cycle("AB"), 5)) # ['A', 'B', 'A', 'B', 'A']
list(repeat(7, 3)) # [7, 7, 7]
Because count and cycle never stop, you must bound them — with islice, zip,
or a break — or your loop runs forever. They're ideal for generating ids,
round-robin assignment, or padding.
chain(*iterables) lazily concatenates multiple iterables into one stream,
without building an intermediate combined list. chain.from_iterable(iter_of_iters)
does the same when the iterables come from a single iterable (e.g. a list of lists).
from itertools import chain
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]
rows = [[1, 2], [3, 4], [5, 6]]
list(chain.from_iterable(rows)) # [1, 2, 3, 4, 5, 6] — flatten one level
It's the memory-friendly way to iterate over several sequences as if they were one, and the idiomatic one-level flatten.
islice(iterable, stop) or islice(iterable, start, stop, step) slices any
iterator lazily — including infinite ones and generators that don't support
[ ] indexing. Unlike list slicing, it can't use negative indices (it can't
look backward in a stream) and it consumes the underlying iterator.
from itertools import islice, count
list(islice(count(), 2, 7)) # [2, 3, 4, 5, 6] — works on an infinite source
gen = (x * x for x in range(10))
list(islice(gen, 3)) # [0, 1, 4] — slice a generator
Use islice to take a window from a stream without materializing it. For a concrete
list where you want negative indices, ordinary seq[a:b] is fine.
These generate combinatorial results lazily. permutations(it, r) —
ordered arrangements (order matters). combinations(it, r) — unordered
selections (order doesn't, no repeats). product(*its) — the Cartesian product
(nested loops), with repeat=n for self-products.
from itertools import permutations, combinations, product
list(permutations([1, 2, 3], 2)) # (1,2)(1,3)(2,1)(2,3)(3,1)(3,2)
list(combinations([1, 2, 3], 2)) # (1,2)(1,3)(2,3)
list(product([0, 1], repeat=2)) # (0,0)(0,1)(1,0)(1,1)
Counts grow fast (factorial / exponential), so keep r and inputs small or consume
lazily. product(a, b) replaces a nested for over two sequences.
groupby groups only consecutive items that share a key — it does not sort
first. So the same key appearing in non-adjacent positions creates multiple
groups. To get one group per key, sort by the same key function first.
from itertools import groupby
data = ["apple", "avocado", "banana", "apricot"]
# WRONG — not sorted: 'a' group splits because 'banana' is between
for k, g in groupby(data, key=lambda s: s[0]):
print(k, list(g)) # a [...] , b [...] , a [apricot]
data.sort(key=lambda s: s[0]) # sort by the SAME key
for k, g in groupby(data, key=lambda s: s[0]):
print(k, list(g)) # a [...], b [...] — correct
Also note each group is a lazy sub-iterator that's invalidated when you advance
to the next group — materialize it with list() if you need it later. Always sort
by the grouping key before groupby.
accumulate(iterable, func=operator.add) yields running totals — each output is
the function applied cumulatively, so by default you get a running sum. Pass a
different binary func for running max, product, etc.
from itertools import accumulate
import operator
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] — running sum
list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] — running product
list(accumulate([3, 1, 4, 1, 5], max)) # [3, 3, 4, 4, 5] — running max
Unlike functools.reduce, which returns only the final value, accumulate
yields every intermediate result lazily. Use it for prefix sums and similar
scans.
Every itertools function returns a lazy iterator that computes items on
demand, so it never holds the whole sequence in memory. This lets you process
huge or infinite streams in constant memory, and chain operations into a
pipeline that only does the work actually consumed.
from itertools import count, islice
# find the first 5 squares over 1000 — from an infinite source
squares = (n * n for n in count(1))
big = (s for s in squares if s > 1000)
print(list(islice(big, 5))) # computed lazily, nothing materialized
sum(islice(count(1), 1_000_000)) # no million-element list built
The trade-off is that iterators are single-pass and not indexable. Reach for
itertools when streaming or composing transformations over large data;
materialize to a list only when you need random access or multiple passes.
It flattens one level of a nested iterable lazily — like chain(*lists)
but without unpacking everything up front, so it works on an infinite or
huge sequence of iterables. Preferred when the outer iterable is itself
lazy.
from itertools import chain
rows = [[1, 2], [3], [4, 5]]
list(chain.from_iterable(rows)) # [1, 2, 3, 4, 5]
# streams without building the arg list that chain(*rows) needs
Rule of thumb: use chain.from_iterable to flatten a stream of iterables;
chain(a, b, c) when you have a few named ones.
tee(it, n) splits one iterator into n independent iterators. The
catch: it buffers items consumed by one branch until the others catch up,
so if branches advance at very different rates it can use lots of memory.
Also, don't use the original iterator after teeing it.
from itertools import tee
a, b = tee(source)
next(a) # b still starts from the beginning (buffered)
Rule of thumb: tee is great for a couple of roughly-in-step passes; if one
branch lags far behind, just materialize to a list.
zip stops at the shortest iterable; zip_longest continues to the
longest, filling missing values with fillvalue (default None).
Use it when you must process every element of unequal-length iterables.
from itertools import zip_longest
list(zip("abc", [1, 2])) # [('a',1),('b',2)]
list(zip_longest("abc", [1, 2], fillvalue=0)) # [('a',1),('b',2),('c',0)]
Rule of thumb: zip to align equal-length data (or deliberately truncate);
zip_longest when you can't afford to drop the tail.
takewhile(pred, it) yields items until the predicate first fails,
then stops. dropwhile(pred, it) skips leading items while the
predicate holds, then yields everything after. They split a stream at the
first boundary — unlike filter, which tests every element.
from itertools import takewhile, dropwhile
data = [1, 2, 3, 1, 0]
list(takewhile(lambda x: x < 3, data)) # [1, 2]
list(dropwhile(lambda x: x < 3, data)) # [3, 1, 0]
Rule of thumb: use these for "stop/skip at the first transition" on sorted or
prefixed data; use filter to test each element independently.
Use starmap when your iterable holds pre-grouped argument tuples —
it calls f(*args) for each. map passes each item as a single argument, so
it can't unpack tuples into multiple parameters.
from itertools import starmap
pairs = [(2, 3), (4, 5)]
list(starmap(pow, pairs)) # [8, 1024] -> pow(2,3), pow(4,5)
# map(pow, pairs) would fail: pow gets one tuple arg
Rule of thumb: map(f, items) for one-arg calls; starmap(f, tuples) when
each element is already an argument tuple.
The complement of filter: it keeps items for which the predicate is
false. It saves writing filter(lambda x: not pred(x), it) and reads more
clearly for "everything that doesn't match".
from itertools import filterfalse
nums = range(6)
list(filterfalse(lambda x: x % 2, nums)) # [0, 2, 4] -> the evens
Rule of thumb: use filterfalse for the "reject matching" case instead of
negating a predicate inside filter.
pairwise(it) (3.10+) yields consecutive overlapping pairs:
(s0,s1), (s1,s2), .... It's the clean way to compare each element with its
neighbor — for diffs, deltas, or detecting transitions — without manual
indexing.
from itertools import pairwise
list(pairwise([1, 4, 9])) # [(1, 4), (4, 9)]
deltas = [b - a for a, b in pairwise(readings)]
Rule of thumb: use pairwise for neighbor comparisons instead of
zip(seq, seq[1:]) or index math.
count(start, step) is an infinite counter (and supports float
steps), while range is finite and integer-only. count is handy as an
endless id generator or paired with zip/islice to number a stream.
from itertools import count, islice
list(islice(count(10, 5), 3)) # [10, 15, 20]
for i, item in zip(count(1), stream): ... # 1-based numbering
Rule of thumb: use count for unbounded or float sequences (bounded with
islice); range for ordinary finite integer loops.
groupby yields (key, group) where group is a lazy sub-iterator tied
to the underlying stream — advancing to the next group invalidates the
previous one. Materialize each group (e.g. list(group)) before moving on,
and remember to sort by the same key first.
from itertools import groupby
data = sorted(items, key=keyfn)
for key, group in groupby(data, key=keyfn):
members = list(group) # consume before next iteration
Rule of thumb: sort by the grouping key first, and list() each group inside
the loop before advancing.
No — islice(it, start, stop, step) accepts only non-negative
values and can't use negative indices/steps (it can't look backward in a
one-pass iterator). It consumes and discards skipped items. For negative
indexing you must materialize to a list and slice normally.
from itertools import islice
list(islice(range(10), 2, 8, 2)) # [2, 4, 6]
islice(range(10), -1) # ValueError
Rule of thumb: islice for forward, non-negative lazy slicing; convert to a
list when you need negative indices or steps.
More Functional Programming interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.