Skip to content

itertools Interview Questions & Answers

17 questions Updated 2026-06-18 Share:

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.

Read the in-depth guidePython itertools Explained — Lazy Iterators for Chaining, Grouping, and Combinatorics(opens in new tab)
17 of 17

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel