Skip to main content

Objects/rangeobject.c (part 5)

Source:

cpython 3.14 @ ab2d84fe1023/Objects/rangeobject.c

This annotation covers range membership testing and indexing. See objects_rangeobject4_detail for range.__new__, range.start/stop/step, and range.__reversed__.

Map

LinesSymbolRole
1-80range.__len__Exact count of elements
81-160range.__contains__O(1) membership test
161-240range.__getitem__Index and slice access
241-360rangeiterobjectFast iterator over range
361-500range.index / range.countSearch and count methods

Reading

range.__len__

// CPython: Objects/rangeobject.c:80 range_length
static PyObject *
range_length(rangeobject *r)
{
return compute_range_length(r->start, r->stop, r->step);
}

static PyObject *
compute_range_length(PyObject *start, PyObject *stop, PyObject *step)
{
/* len = max(0, ceil((stop - start) / step)) */
PyObject *diff = PyNumber_Subtract(stop, start);
PyObject *result = PyNumber_FloorDivide(diff, step);
/* Adjust for ceiling division */
...
}

len(range(1, 10, 2)) is 5 (values: 1, 3, 5, 7, 9). The calculation handles negative steps: len(range(10, 0, -2)) is also 5.

range.__contains__

// CPython: Objects/rangeobject.c:140 range_contains
static int
range_contains_long(rangeobject *r, PyObject *ob)
{
/* Check: start <= ob < stop (or start >= ob > stop for negative step) */
/* Check: (ob - start) % step == 0 */
int cmp1, cmp2, cmp3;
PyObject *tmp = PyNumber_Subtract(ob, r->start);
PyObject *mod = PyNumber_Remainder(tmp, r->step);
int is_member = (PyObject_RichCompareBool(mod, _PyLong_Zero, Py_EQ)
&& in_bounds);
...
}

42 in range(0, 100, 3) does not iterate — it computes in O(1) using modular arithmetic: check bounds and check (42 - 0) % 3 == 0. This makes range useful as a set-of-integers alternative for membership tests.

range.__getitem__

// CPython: Objects/rangeobject.c:200 range_item
static PyObject *
range_item(rangeobject *r, Py_ssize_t i)
{
Py_ssize_t len = PyLong_AsSsize_t(range_length(r));
if (i < 0) i += len;
if (i < 0 || i >= len) {
PyErr_SetString(PyExc_IndexError, "range object index out of range");
return NULL;
}
/* result = start + i * step */
PyObject *idx = PyLong_FromSsize_t(i);
PyObject *scaled = PyNumber_Multiply(idx, r->step);
return PyNumber_Add(r->start, scaled);
}

range(2, 20, 3)[4] = 2 + 4*3 = 14. Negative indexing wraps around. Slice of a range returns a new range object (O(1), not O(n)).

rangeiterobject

// CPython: Objects/rangeobject.c:320 rangeiter_next
static PyObject *
rangeiter_next(rangeiterobject *r)
{
if (r->index >= r->len) return NULL; /* StopIteration */
/* current = start + index * step */
long result = r->start + r->index * r->step;
r->index++;
return PyLong_FromLong(result);
}

The fast path uses C long arithmetic when start, stop, step all fit in a C long. For large ranges (bigint), it falls back to PyNumber_Add/PyNumber_Multiply. The fast path is the common case.

range.index

# CPython: Objects/rangeobject.c:440 range_index
def index(self, value):
if value not in self:
raise ValueError(f'{value} is not in range')
# Compute: (value - start) // step
return (value - self.start) // self.step

range(2, 20, 3).index(14) returns 4. Like __contains__, it is O(1) using arithmetic, not a linear scan.

gopy notes

range.__len__ is objects.Range.Len in objects/range.go. __contains__ computes (x - start) % step == 0 using big.Int for safety. __getitem__ is objects.Range.GetItem. rangeiterobject is objects.RangeIter with a index int64 field.