Iterator performance in practice

itertools · Expert · 10 min

Hot loops over millions of items benefit from pushing work into C-implemented iterators.

itertools functions, map, filter, zip, sum and friends are implemented in C. When a pipeline uses only C-level callables, the whole loop runs without executing Python bytecode per item, which can be several times faster than a generator expression.

Consequences:

  • map(int, strings) or map(operator.mul, xs, ys) is fast: both the loop and the function are in C. map(lambda x: x * 2, xs) loses most of the advantage, since the lambda runs as Python code; a comprehension is then just as fast and more readable.
  • The operator module (itemgetter, attrgetter, mul) provides C-level callables for common lambdas.
  • sum(map(operator.mul, xs, ys)) computes a dot product without a Python-level loop; math.sumprod (3.12+) is faster still.
  • Consuming with deque(it, maxlen=0), sum, max or "".join keeps the consumer in C too.

Laziness itself has a cost: each stage adds a little per-item overhead. For small data, a list comprehension that materialises results is often fastest. For large data, laziness wins on memory and often on cache behaviour.

Measure with timeit on realistic sizes, and remember that for numeric arrays, NumPy's vectorised operations beat any iterator pipeline by an order of magnitude.

Note: copying, deep-copying and pickling itertools objects was deprecated in 3.12 and removed in 3.14; don't rely on it.

Example

import math
import operator
import timeit

xs = list(range(1, 200_001))
ys = list(range(200_000, 0, -1))

t_genexp = timeit.timeit(lambda: sum(x * y for x, y in zip(xs, ys)), number=10)
t_map_op = timeit.timeit(lambda: sum(map(operator.mul, xs, ys)), number=10)
t_sumprod = timeit.timeit(lambda: math.sumprod(xs, ys), number=10)
print(t_map_op < t_genexp, t_sumprod < t_map_op)   # True True

words = [str(i) for i in range(100_000)]
t_lambda = timeit.timeit(lambda: list(map(lambda s: int(s), words)), number=10)
t_builtin = timeit.timeit(lambda: list(map(int, words)), number=10)
print(t_builtin < t_lambda)                         # True
print(sum(map(operator.mul, [1, 2, 3], [4, 5, 6])), math.sumprod([1, 2, 3], [4, 5, 6]))  # 32 32

Remember

  • Pipelines made only of C callables avoid per-item Python bytecode
  • map with a lambda is no faster than a comprehension; use operator functions
  • Measure; for numeric arrays, NumPy beats iterator pipelines

Watch out for

  • Rewriting comprehensions as map(lambda ...) expecting a speedup.

Check yourself

  1. Which is typically fastest for a dot product of two large Python lists in 3.13?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/itertools.html

Builds on

Unlocks

Nothing builds on this yet.