itertools Interview Questions & Answers

7 questions Updated 2026-06-18

Python interview questions on itertools — count/cycle/repeat, chain, islice, combinations/permutations/product, groupby's sorted-input rule, accumulate, and the memory benefit of lazy iterators.

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.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.