itertools module
Monty implements the whole of itertools, matching CPython 3.14 for
arguments, values, repr() and error messages apart from the notes below.
Every name CPython’s itertools exports, including the private
_grouper, _tee and _tee_dataobject. As in CPython, all of them are type
objects except tee, which is a plain function — so isinstance(x, count),
type(x) is chain and chain[int] all work.
repeat.__length_hint__()raisesAttributeError. CPython exposes the number of remaining yields through it (repeat(9, 3).__length_hint__() == 3). Monty uses the remaining count internally to size the target oflist()/tuple(), but does not expose it as a Python-visible attribute.countandrepeatobjects are unhashable.hash(itertools.count())raisesTypeError: unhashable type: 'itertools.count', where CPython falls back to identity hashing. This applies to Monty’s iterators generally, not just these two.countaccepts onlyint,floatandbool. CPython accepts anything satisfyingPyNumber_Check(e.g.Decimal,Fraction, complex). Monty has no other numeric types, so the sameTypeError: a number is requiredcovers them all.- Nested-cycle
repr()unwinds one level earlier. For a container that reaches back to therepeatholding it, Monty printsrepeat([...])where CPython printsrepeat([repeat([...])]). This is Monty’s general cycle detection inrepr(), not specific toitertools. - A callable that suspends is rejected, not paused.
takewhile,dropwhile,filterfalse,starmap,accumulateandgroupbyapply their callable through the synchronousevaluate_functionpath, which runs a frame to completion and cannot yield to the host. A callable that reaches an external function, anosoperation, or a host method call therefore raisesNotImplementedError, naming the adaptor that called it —takewhile(): external function 'f' is not yet supported in this contextfromtakewhile,groupby(): ...fromgroupby, and so on. CPython would simply call it. This is the same restriction that applies to__init__,__next__and__repr__(see classes.md); ordinary sandbox-defined functions and lambdas are unaffected. zip_longestnames the rejected keyword. A bad keyword raiseszip_longest() got an unexpected keyword argument 'bogus', where CPython raiseszip_longest() got an unexpected keyword argumentwith no name — CPython hand-rolls that check rather than going through a parser family, and is alone in omitting it. Every other adaptor’s wording matches.- A re-entrant
zip_longeststops instead of padding forever. A source that steps the samezip_longestfrom inside its own__next__can exhaust the remaining slots before the outer round reaches them. Both agree on the row that outer round yields; from the next one on Monty raisesStopIteration, having seen every slot go spent, while CPython drives itsnumactivecount below zero and so yields an all-fillvaluetuple on every laternext(), without end. Only re-entrancy reaches this: an ordinary source cannot run while the adaptor that owns it is mid-round. - Crossing the host boundary loses the repr. A
count/repeatobject returned to the host arrives as<itertools.count object>/<itertools.repeat object>rather than its in-sandboxrepr()(count(0),repeat(7, 3)). Monty represents all iterators this way rather than recursing into what they hold. batched’snis bounded by the worker’s pointer width, so the wasm worker raisesOverflowError: Python int too large to convert to C ssize_tfornat or above2**31, where CPython accepts it.batched’s type cannot cross to a host below Python 3.12, which is whereitertools.batchedwas added. Returningtype(itertools.batched(...))to such a host raisesTypeError: Cannot convert itertools.batched to a host type: this Python does not define it. Every other adaptor’s type resolves on all supported hosts.chain.from_iterableis a plain function, not a bound classmethod. CPython builds a new bound method object per attribute access, sorepr(itertools.chain.from_iterable)is<built-in method from_iterable of type object at 0x...>anditertools.chain.from_iterable is itertools.chain.from_iterableisFalse. Monty resolves the attribute to one function value, so thereprreads<function from_iterable at 0x...>and the identity check isTrue. The attribute is also reachable only through thechaintype itself: an instance does not carry it, soitertools.chain([1]).from_iterable([[2]])raisesAttributeError: 'itertools.chain' object has no attribute 'from_iterable'where CPython accepts it. Its element type is also lost in type checking, since the vendored typeshed stub declares it as a classmethod.- The private types cannot be constructed.
_grouper,_teeand_tee_dataobjectare exposed under their CPython names, sotype()andisinstance()work, but calling one raisesTypeError: cannot create 'itertools._tee' instances. CPython builds them from the arguments its internals use (_tee([1, 2])gives a working iterator). They are only ever handed out bygroupbyandteehere. groupbynever releases its source. This matches CPython, but note that the resource limits apply to the skip between groups: a source whose key never changes (groupby(repeat(1))) makes the secondnext()scan forever, and is stopped by a duration limit rather than running to completion.- The combinatoric iterators collect their input at construction. CPython
does the same (its
poolis a tuple built byPySequence_Tuple), socombinations,combinations_with_replacement,permutationsandproductall consume the whole iterable before the firstnext()and raise there for a non-iterable or a raising source. The consequence worth naming is that an infinite input never returns:permutations(count())runs until a resource limit trips on both engines. product’srepeatandcombinations_with_replacement’srare preflighted againstmax_memory. Each sizes a result wider than the input it was given, so under a memory limit a large value (product('ab', repeat=10**9)) raisesMemoryErrorat construction, where CPython raises only once an allocation actually fails. An empty pool is exempt, since it empties the product before anything is sized.- An
rtoo large to build indices for yields nothing instead of raising.combinations('a', 2**62)andpermutations('a', 2**62)are empty iterators in Monty, because anrpast the pool is known to yield nothing before any vector is sized. CPython allocates the vector first and so raisesMemoryError. The two calls that genuinely need the vector agree with CPython:combinations_with_replacement('a', 2**62)raisesMemoryError, andproduct('ab', repeat=2**62)raisesOverflowError: repeat argument too large. All four are 64-bit worker behaviour: likebatched’sn,randrepeatare bounded by the worker’s pointer width, so the wasm worker raisesOverflowError: Python int too large to convert to C ssize_tfor anything at or above2**31before reaching any of it. productnames a rejected keyword instead of counting keywords. Two keywords where one is unknown (product([1], repeat=2, bogus=1)) raiseproduct() got an unexpected keyword argument 'bogus', where CPython raisesproduct() takes at most 1 keyword argument (2 given). A single unknown keyword reads the same on both.- A
groupbykey comparison that re-enters its owngroupbykeeps going. A user__eq__that steps the samegroupbyand consumes the pair being compared leaves CPython reading through freed state, where it segfaults. Monty reads the next pair instead, which is what CPython’s own loop condition intends, so the call ends in an ordinaryStopIteration.
map(), filter() and enumerate() are eager in Monty: each drains its
source into a list and returns a concrete result, rather than the lazy iterator
CPython returns. Applied to an infinite itertools iterator they therefore
never return, where CPython yields lazily:
import itertools
map(str, itertools.count()) # CPython: lazy. Monty: runs until a limit trips.
filter(bool, itertools.repeat(1)) # likewise
enumerate(itertools.count()) # likewise
zip() stops at the shortest input, so zip(itertools.count(), 'ab') behaves
as in CPython, as does slicing an infinite iterator by hand via next(). This
is a pre-existing property of those builtins rather than something itertools
introduces, but count()/repeat() are the first easy way for sandboxed code
to reach it.
count() and repeat(x) are infinite, so consuming one without a bound
(list(itertools.count())) only terminates if the host has configured a memory
or duration limit, and then raises MemoryError rather than exhausting. Under
ResourceLimits::default(), which sets neither (only a recursion depth), it
runs until the host itself runs out of memory. This is the same exposure as a
while True: loop, not something specific to itertools.
The adaptors that discard items without yielding — dropwhile and
filterfalse before their first accepted item, compress past a falsy run,
islice skipping to start, chain crossing an exhausted source — poll
the duration limits themselves while looping, so a discarding pass over an
infinite source raises TimeoutError instead of spinning. The poll is amortized (once
per 64 items), so the limit can be overshot by up to that much work. CPython
has no duration limit at all and would loop forever.
batched(iterable, n) fills a whole batch inside one next(), so a large n
over a long source is the same kind of non-yielding loop and polls the
duration limits the same way. It also preflights one batch against max_memory
from the source’s size hint, capped at n — an exact-hint source
(batched(range(10**9), 10**9)) therefore raises MemoryError up front rather
than while filling. A source with no size hint gets no preflight, so the fill
loop polls max_memory as well as the duration limits on the same amortized
cadence — batched(count(), 10**9) raises MemoryError while filling.
cycle(iterable) must buffer every item it has seen so far in order to replay
them, and that buffer is charged against max_memory as it grows, so cycling
over a very long source raises MemoryError at the limit rather than at the
point the source is exhausted. CPython buffers the same items with no such
ceiling.
Nesting the source-driving adaptors — everything except count and repeat —
is bounded by max_recursion_depth: an adaptor charges one recursion level
while delegating next() to its wrapped iterator, so a nest deeper than the
limit raises RecursionError when consumed. An adaptor answering from its own
state charges nothing, since it touches no source: a spent batched, a latched
takewhile, an accumulate yielding its initial. CPython imposes no
comparable per-adaptor bound; deep nesting there is limited only by the C
stack.