How do you size an itertools.product space before a nightly billing run enumerates it?
answer
- Flat memory, one hot core, no error
- Arithmetic before profiler
- math.prod, math.comb, math.perm
- Count times per-tuple cost equals wall clock
- Filter the inputs, not the output tuples
basics
~20 sCompute the output count before iterating: math.prod of the input lengths for itertools.product, n**k when repeat=k, math.comb for combinations and math.perm for permutations. Laziness bounds memory, never runtime, so the count is the only early warning.
solid answer
~50 sThe first move is arithmetic, not profiling. Every combinatorial iterator has a closed-form size: `math.prod([len(x) for x in inputs])` for `itertools.product`, `n ** k` when you pass `repeat=k`, `math.comb(n, r)` for `itertools.combinations`, `math.perm(n, r)` for `itertools.permutations`. Multiply that count by a measured per-tuple cost and you have the run time before you start. This matters because these iterators are lazy: a run that will never finish looks perfectly healthy in memory and never raises, so nothing warns you. When the number is too large, the fix is structural — shrink the alphabets, deduplicate the inputs, drop dimensions that do not affect the result, or filter the input lists before taking the product rather than filtering tuples afterwards. Log the computed count at the start of the job so the failure is visible in the first line of output rather than in the morning.
code
python · 10 linesimport math
from itertools import product
flag_lists = [["a", "b", "c"], [0, 1, 2, 3], [True, False], list(range(5))]
total = math.prod(len(f) for f in flag_lists)
print(f"enumerating {total} tuples")
if total > 1_000_000:
raise SystemExit("space too large; refusing to start")
for combo in product(*flag_lists):
passgo deeper
Know that itertools.product output grows as the sizes multiplied together, and that you can compute that number with math.prod before writing the loop.
Explain the closed-form counts for product, permutations and combinations, and why a lazy iterator can run forever while memory stays flat.
Diagnose from the symptom set — flat memory, one saturated core, no exception — then convert count times per-tuple cost into a wall-clock estimate and propose structural cuts.
Own the guardrail: a computed size logged and asserted at job start, plus a team convention that any combinatorial enumeration states its bound in review, so growth in an input list cannot silently break a nightly window.
### The scenario A nightly subscription-billing reconciliation walks candidate groupings of fee flags with `itertools.product` and stops finishing inside its window. The 4-person team that owns the job first suspects an encoding mismatch, because the fee-code file is read from an upstream export and a `str`/`bytes` confusion had bitten them before. It is a reasonable first guess and it is wrong: the job is not stuck on a decode, it is enumerating a space nobody counted. This failure has a signature. Memory is flat, the process is at 100% of one core, no exception is ever raised, and adding logging shows tuples streaming past steadily. That combination — healthy memory, no error, no end — is what an oversized lazy enumeration looks like, and it is why the diagnosis is arithmetic rather than a profiler. ### Count first, always Every combinatorial iterator in `itertools` has a closed-form output size, and the standard library computes all of them: ```python import math math.prod([3, 4, 2, 5]) # 120 -> product() of lists with these lengths 10 ** 8 # product(range(10), repeat=8) math.comb(52, 5) # 2598960 -> combinations(deck, 5) math.perm(10, 3) # 720 -> permutations(items, 3) math.factorial(12) # 479001600 -> permutations(items) with r omitted ``` The two shapes of growth are worth naming out loud, because they fail differently. `product` with `repeat=k` is **exponential in k**: adding one more flag doubles a boolean space. `permutations` with `r` omitted is **factorial in n**: 12 items is under half a billion, 15 items is 1.3 trillion, and 20 items is beyond any wall clock. A space that was fine last quarter grows because someone added one element to a list, and the enumeration's shape means one element is not a small change. ### Turning a count into an estimate Time the loop body on a small slice of the space, then multiply. If each tuple costs 20 microseconds and the count is 10**8, that is roughly 2,000 seconds of pure body time before any I/O — already outside a nightly window. Doing this multiplication takes a minute and answers the question that profiling the running job cannot, because the profiler tells you where time goes, not how much of the space remains. Make the count a first-class output of the job. A single line logged at startup — the computed number of tuples, the input lengths that produced it, and the repeat count — turns a silent overnight hang into an obvious number a reviewer can react to. Better still, assert against a ceiling and fail fast: a job that refuses to start above a configured limit is far kinder than one that quietly runs until it is killed. ### What to do when the number is too big Laziness will not save you. `itertools.product` yields one tuple at a time and holds only the input tuples plus the current result, so memory is never the constraint — the constraint is that the loop body runs once per tuple. The fixes are structural: * **Shrink the alphabets.** Deduplicate each input list; equal values in a list multiply the count without adding information. Collapse values that are equivalent for the computation. * **Drop dimensions.** A flag that does not change the outcome should not be in the product at all. Enumerating over it multiplies the whole space by its length for nothing. * **Filter before, not after.** A `product` over pre-filtered input lists is exponentially smaller than a `product` over full lists with a filter on the output tuples, because every rejected element is removed from every position it would have occupied. * **Split the enumeration.** If a condition depends on only the first two positions, enumerate those two, test, and only then take the product of the rest for survivors. This is the point where a hand-written nested loop can beat `product`, because it can skip whole subtrees rather than generating them. * **Reconsider whether enumeration is the right model at all.** Many spaces that look combinatorial have a direct or greedy formulation, and the interviewer will be glad you asked the question. ### Getting the pruning boundary right One caution about splitting: an enumeration that is exponential stays exponential after pruning unless the pruning removes a dimension rather than a fraction of the tuples. Cutting 90% of a 10**8 space still leaves 10**7 items. Prune where it removes a factor, not where it removes a slice. ### What good looks like in the answer The strong response has four beats: recognize the symptom (flat memory, one hot core, no exception, no end); reach for the closed-form count instead of a profiler; convert count times per-tuple cost into a wall-clock estimate; and then propose structural fixes plus a startup log or assertion so the same failure is loud next time. Naming `math.prod`, `math.comb` and `math.perm` shows you know the counting is a one-liner, and saying plainly that **laziness bounds memory rather than time** is the sentence that separates a candidate who has operated one of these jobs from one who has only read about iterators.
- The job's memory graph is flat while it hangs. Why does that rule out a materialized result list?Because building a list of results would grow the heap in step with the loop, and a lazy `itertools` iterator does not: it holds the input tuples plus one output tuple. Flat memory with a saturated core and no exception is the fingerprint of an oversized lazy enumeration — the work is real, the count is unknown, and nothing will ever raise to tell you.
- Why is filtering the input lists before itertools.product better than filtering the yielded tuples?Removing one value from an input list removes it from every tuple that would have contained it, cutting the count by a whole factor. Filtering output tuples still generates each one first, so you pay the full enumeration and only save the loop body. The first is a change in the exponent's base; the second is a constant-factor saving.
- How would you make this failure loud rather than silent next time?Compute the count at startup with `math.prod` over the input lengths, log it together with the input sizes, and assert it against a configured ceiling so an oversized run refuses to start instead of grinding all night. Pair that with a timed sample of the loop body so the log carries an estimated duration, not just a count.
A lazy enumerator is a conveyor belt with an unknown number of boxes behind the wall: it never overflows the room, and it never tells you when it stops.
saying these in an interview costs you the question
- Reaches for a profiler before computing the output count
- Says laziness makes the enumeration size irrelevant
- Blames memory when the memory graph is flat
- Filters the yielded tuples instead of the input lists
- Cannot name a closed-form count for product or combinations
- Assumes pruning a fraction fixes exponential growth