itertools is the lazy module, so every function in it is lazy. Two of them
are not, in the way that matters.
tee: the buffer is the whole stream
tee(iterable, n) gives you n independent iterators over one source. It
does that by keeping an internal buffer of every item that some consumer has
seen and another has not yet reached.
If you interleave the consumers, the buffer stays small — that is the intended use, and it is genuinely useful for “look one item ahead while still processing this one”.
If you drain one side first:
a, b = tee(source)
everything = list(a) # b's buffer now holds the entire stream
for item in b: # reads from a full in-memory buffer
...
you have materialised the whole stream plus paid for the tee machinery.
That is strictly worse than data = list(source) and iterating data twice —
same memory, more indirection, more surprise. The docs say this outright, and
it is still the most common misuse of the function.
There is a second rule that catches people: after teeing, do not use the original iterator. The tees assume they own it. Advancing the source directly makes items disappear from some or all of the tees.
💡a, b, c = tee(source, 2) — what does mypy say, and what happens
click to reveal
at runtime?
mypy says nothing. tee is typed tuple[Iterator[T], ...] — a variadic
tuple, because the arity depends on the runtime value of n, which a type
cannot express. Unpacking a variadic tuple into any number of names
type-checks.
At runtime it raises ValueError: not enough values to unpack (expected 3, got 2).
This is a signature that cannot state its own contract, and it is worth
recognising the shape: whenever the number of results depends on an argument
value, the return type degrades to variadic and unpacking stops being
checked. tee and batched are both instances. A checker cannot save you;
reading the signature can.
product: eager inputs, exponential output
product is lazy in its output and eager in its inputs. It has to be: to
produce the second element of the first tuple it needs to have seen all of the
second iterable, so it converts every argument to a tuple up front, before
yielding anything.
product(giant_generator, [1, 2]) # consumes giant_generator entirely, immediately
And the output grows multiplicatively. product(range(100), repeat=4) is
$100^4 = 10^8$ tuples. Nothing warns you; the expression is four tokens long
and the loop simply never finishes.
The repeat= argument is the sharp edge, because it is the one place where a
small edit to a constant changes the running time by orders of magnitude.
repeat=3 to repeat=5 on a 50-element input is 125,000 to 312 million.
Where product earns its place: replacing a nest of for loops whose depth
is known and small — parameter sweeps, coordinate grids, truth tables. If
you are tempted to write it over an input whose size you do not control, you
want a sampler or a search, not an enumeration.
💡You need to check every pair of items in a stream for a conflict. click to reveal
product(stream, stream) is wrong for two separate reasons. What are they,
and what do you write?
First, stream is one iterator. product consumes it into a tuple for its
first argument, at which point the second argument is an exhausted iterator
and the product is empty. You get no pairs and no error.
Second, even with a list, product(items, items) gives $n^2$ ordered pairs
including every (x, x) and both (a, b) and (b, a). For a symmetric
conflict check that is twice the work plus $n$ meaningless self-comparisons.
What you want is itertools.combinations(items, 2): $\binom{n}{2}$ unordered
pairs, no self-pairs, and it takes a sequence so the “one iterator, two
positions” problem cannot arise. It is still quadratic — if $n$ is large the
real answer is to index or bucket the items so you only compare candidates
that could conflict.
The general rule
In itertools, laziness is per-function, not per-module. Before using one on
a stream you cannot afford to materialise, ask two questions: does it need to
see all of its input before producing its first output? and does it retain
what it has already yielded? chain, islice, takewhile and pairwise
answer no to both. product, permutations and combinations answer yes to
the first. tee and cycle answer yes to the second.