Skip to main content

Objects/rangeobject.c

range is a lazy, immutable arithmetic sequence. Every operation — length, indexing, slicing, membership — is computed directly from start, stop, and step with no backing array. CPython 3.x ranges hold arbitrary Python integers, not C longs, so all arithmetic uses PyLong operations.

Map

LinesSymbolRole
1–60rangeobject layoutstart, stop, step, length fields (all PyObject *)
61–180range_newConstructor; validates args, normalises empty range
181–230compute_range_lengthInteger length formula with overflow guard
231–290range_length__len__; delegates to compute_range_length
291–360range_item__getitem__ by integer index; negative index wrap
361–460range_slice__getitem__ by slice; returns a new range
461–560range_containsO(1) membership test via modular arithmetic
561–700range_iter / rangeiterobjectFast iterator with C-long fast path
701–1100Remaining methods__repr__, __reduce__, count, index, __reversed__

Reading

range_new — empty-range normalisation

When step is positive but stop <= start, or step is negative but stop >= start, the range is empty. range_new detects this and stores a canonical empty range rather than leaving inconsistent start/stop values that would confuse length arithmetic later.

// CPython: Objects/rangeobject.c:95 range_new
static PyObject *
range_new(PyTypeObject *type, PyObject *args, PyObject *kw)
{
...
/* Validate step != 0 */
cmp = PyObject_RichCompareBool(step, _PyLong_Zero, Py_EQ);
if (cmp > 0) {
PyErr_SetString(PyExc_ValueError,
"range() arg 3 must not be zero");
goto error;
}
...
}

compute_range_length — overflow-safe length

The mathematical length of range(start, stop, step) is max(0, ceil((stop - start) / step)). Because start, stop, and step are arbitrary Python ints, the subtraction and division cannot overflow, but the result must still fit in Py_ssize_t before it can be returned as a sequence length.

// CPython: Objects/rangeobject.c:195 compute_range_length
static PyObject *
compute_range_length(PyObject *start, PyObject *stop, PyObject *step)
{
/* length = (stop - start + step + (-1 if step>0 else 1)) / step */
PyObject *tmp1, *tmp2, *result;
int cmp_result;

cmp_result = PyObject_RichCompareBool(step, _PyLong_Zero, Py_GT);
...
tmp1 = PyNumber_Subtract(stop, start);
...
result = PyNumber_FloorDivide(tmp2, step);
...
return result;
}

range_item — index arithmetic

range_item multiplies the normalised index by step, adds start, and returns the result as a new Python int. Negative indices are wrapped by adding length before the multiply.

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

range_contains — O(1) membership

Membership is tested without iterating: a value v is in range(start, stop, step) when it is reachable by the step arithmetic. The check is: v >= start (for positive step), (v - start) % step == 0, and the quotient is less than length. For non-integer arguments it falls back to the generic sequence scan.

// CPython: Objects/rangeobject.c:493 range_contains_long
static int
range_contains_long(PyRangeObject *r, PyObject *ob)
{
PyObject *tmp1, *tmp2;
int result = 0;
/* Check ob >= start (positive step) or ob <= start (negative step) */
...
tmp1 = PyNumber_Subtract(ob, r->start);
tmp2 = PyNumber_Remainder(tmp1, r->step);
result = PyObject_RichCompareBool(tmp2, _PyLong_Zero, Py_EQ);
...
return result;
}

gopy notes

  • All five fields (start, stop, step, length, and the iterator's index) are PyObject * in CPython because they can be arbitrarily large ints. A Go port using int64 would silently truncate large ranges; use *big.Int or the gopy objects.Int wrapper.
  • compute_range_length should be ported as a standalone helper tested independently before any of the sequence methods call it.
  • range_iter has an internal C-long fast path that bypasses Python-int arithmetic when start, step, and length all fit in long. Port that optimisation last, after the slow path is verified correct.
  • range_contains is the most frequently called method in numeric loops; get the modular-arithmetic edge cases (negative step, zero delta) right before marking it done.

CPython 3.14 changes

  • The rangeiterobject gained __copy__ / __deepcopy__ so that copy.copy(iter(range(n))) returns an independent iterator at the same position, consistent with other iterator types.
  • range.__class_getitem__ now returns a types.GenericAlias, allowing range[int] in type annotations without raising TypeError.
  • Internal use of _PyLong_IsZero and the new small-int fast paths in Objects/longobject.c means that range(0, n, 1) construction avoids several heap allocations compared with 3.12.