Skip to main content

Modules/itertoolsmodule.c

Source:

cpython 3.14 @ ab2d84fe1023/Modules/itertoolsmodule.c

itertoolsmodule.c implements the entire itertools module as a collection of stateful C iterator types. Each type has an __iter__ that returns itself and an __next__ (the tp_iternext slot) that advances internal state and produces the next value. The file is large because each iterator is self-contained: struct, tp_new, tp_dealloc, tp_iternext, and __reduce__ are all defined inline.

Map

SymbolKindLines (approx)Purpose
chain_object / chain_iternextstruct + iternext160-280Exhaust one iterator, advance to next
islice_object / islice_nextstruct + iternext900-1050Skip counter and stop sentinel
product_object / product_nextstruct + iternext1300-1500State array and carry-increment loop
groupby_object / groupby_nextstruct + iternext1700-1900Advance past equal keys
accumulate_object / accumulate_nextstruct + iternext2100-2280Initial sentinel, binary function call
combinations_objectstruct2400-2600Index array, lexicographic advance
permutations_objectstruct2700-2900In-place Fisher-Yates advance
cycle_objectstruct600-720Save-to-list then replay
repeat_objectstruct750-860Fixed count or infinite
module initPyModuleDef4460-4500Type registration

Reading

chain_iternext: exhaust and advance

chain holds a tuple of source iterables and a source pointer to the currently active iterator. When the active source raises StopIteration, chain_iternext replaces it with the next iterable from the tuple and retries. When the tuple is exhausted it returns NULL without setting an exception, which signals StopIteration to the caller.

// CPython: Modules/itertoolsmodule.c:214 chain_iternext
static PyObject *
chain_iternext(chainobject *lz)
{
PyObject *item;

while (lz->source != NULL) {
item = (*Py_TYPE(lz->source)->tp_iternext)(lz->source);
if (item != NULL)
return item;
/* Source exhausted: clear exception, advance tuple index */
if (PyErr_ExceptionMatches(PyExc_StopIteration))
PyErr_Clear();
else if (PyErr_Occurred())
return NULL;
Py_CLEAR(lz->source);
lz->source = next_source_from_tuple(lz);
}
return NULL; /* all sources exhausted */
}

chain.from_iterable uses a lazy variant that pulls the inner iterables one at a time from an outer iterator, rather than pre-building the tuple.

islice_next: skip counter and stop sentinel

islice needs to handle start, stop, and step arguments. The cnt field tracks how many items have been yielded. Items before start are consumed and discarded. Items at or beyond stop cause the iterator to return NULL.

// CPython: Modules/itertoolsmodule.c:968 islice_next
static PyObject *
islice_next(isliceobject *lz)
{
PyObject *item;
PyObject *it = lz->it;
Py_ssize_t stop = lz->stop;

if (lz->cnt > stop)
return NULL;

/* Skip ahead to the next yield point */
Py_ssize_t oldnext = lz->next;
while (lz->cnt < oldnext) {
item = (*Py_TYPE(it)->tp_iternext)(it);
if (item == NULL) return NULL;
Py_DECREF(item);
lz->cnt++;
}
item = (*Py_TYPE(it)->tp_iternext)(it);
if (item == NULL) return NULL;
lz->cnt++;
lz->next += lz->step;
return item;
}

When stop is PY_SSIZE_T_MAX the iterator runs until the underlying source is exhausted, matching the behaviour of islice(it, start, None).

product state array and carry increment

product computes the Cartesian product of its input pools. The state is an array of indices (one per pool) that is advanced by a carry-increment loop starting from the rightmost pool, mirroring how an odometer rolls over.

// CPython: Modules/itertoolsmodule.c:1440 product_next
static PyObject *
product_next(productobject *lz)
{
PyObject **pools = lz->pools;
Py_ssize_t *indices = lz->indices;
Py_ssize_t npools = lz->npools;

/* Build result tuple from current indices */
PyObject *result = PyTuple_New(npools);
for (Py_ssize_t i = 0; i < npools; i++) {
PyObject *pool = pools[i];
PyTuple_SET_ITEM(result, i,
Py_NewRef(PyTuple_GET_ITEM(pool, indices[i])));
}

/* Advance: carry from the right */
Py_ssize_t i = npools - 1;
while (i >= 0) {
indices[i]++;
if (indices[i] < PyTuple_GET_SIZE(pools[i]))
break;
indices[i] = 0;
i--;
}
if (i < 0) lz->stopped = 1;
return result;
}

All pools are pre-converted to tuples in product_new, so index access inside product_next is O(1) and never calls Python.

groupby_next: advance past equal keys

groupby groups consecutive elements that share the same key. Its state is the current target key value (tgtkey) and the last item fetched (currvalue). On each call to groupby_next it advances past any items that are equal to the previous group's key before starting a new _grouper sub-iterator.

// CPython: Modules/itertoolsmodule.c:1820 groupby_next
static PyObject *
groupby_next(groupbyobject *gbo)
{
PyObject *newvalue, *newkey, *r;

/* Advance until the key changes */
while (gbo->currkey == NULL ||
PyObject_RichCompareBool(gbo->currkey, gbo->tgtkey, Py_EQ)) {
newvalue = (*Py_TYPE(gbo->it)->tp_iternext)(gbo->it);
if (newvalue == NULL) return NULL;
Py_XDECREF(gbo->currvalue);
gbo->currvalue = newvalue;
newkey = PyObject_CallOneArg(gbo->keyfunc, newvalue);
if (newkey == NULL) return NULL;
Py_XDECREF(gbo->currkey);
gbo->currkey = newkey;
}
Py_INCREF(gbo->currkey);
Py_XDECREF(gbo->tgtkey);
gbo->tgtkey = gbo->currkey;
r = PyTuple_Pack(2, gbo->tgtkey, grouper_new(gbo));
return r;
}

The _grouper sub-iterator holds a reference back to the parent groupby object and stops as soon as the key changes, leaving currvalue in place for the parent to pick up next time.

accumulate_next: initial sentinel and binary function call

accumulate supports an optional initial value and an optional func argument. The initial sentinel is indicated by a NULL total field; on the first call total is set to initial (or the first element) without consuming func.

// CPython: Modules/itertoolsmodule.c:2220 accumulate_next
static PyObject *
accumulate_next(accumulateobject *lz)
{
PyObject *val, *newtotal;

if (lz->total == NULL) {
/* First call: return initial or first element */
if (lz->initial != Py_None) {
lz->total = Py_NewRef(lz->initial);
return Py_NewRef(lz->total);
}
val = (*Py_TYPE(lz->it)->tp_iternext)(lz->it);
if (val == NULL) return NULL;
lz->total = val;
return Py_NewRef(lz->total);
}

val = (*Py_TYPE(lz->it)->tp_iternext)(lz->it);
if (val == NULL) return NULL;

if (lz->binop == Py_None || lz->binop == NULL)
newtotal = PyNumber_Add(lz->total, val);
else
newtotal = PyObject_CallFunctionObjArgs(lz->binop, lz->total, val, NULL);

Py_DECREF(val);
if (newtotal == NULL) return NULL;
Py_SETREF(lz->total, newtotal);
return Py_NewRef(lz->total);
}

When no func is provided the default is operator.add, but the implementation detects lz->binop == NULL and calls PyNumber_Add directly to avoid the attribute lookup overhead.

gopy notes

Status: not yet ported.

Planned package path: module/itertools/. Each iterator type maps to a Go struct implementing the objects.Iterator interface. The carry-increment loop in product_next translates directly to a Go for loop over a []int index slice. groupby requires particular care because the _grouper sub-iterator shares mutable state with the parent; the Go port will use a shared pointer to a common state struct rather than back-references. chain.from_iterable will be a separate type from chain to keep the lazy outer-iterator path clean.