Skip to main content

Objects/sliceobject.c: Slice and Ellipsis Objects

Objects/sliceobject.c covers two distinct things: the Ellipsis singleton (...) and the slice built-in type. The file is mid-sized at ~400 lines but punches above its weight because PySlice_Unpack and PySlice_AdjustIndices are called by every sequence that supports extended slicing.

Map

LinesSymbolRole
1-40Py_EllipsisSingleton object and PyEllipsis_Type
41-100PySlice_NewAllocate PySliceObject, store start/stop/step as-is (may be None)
101-200PySlice_UnpackResolve None to defaults, clamp integers to [PY_SSIZE_T_MIN, PY_SSIZE_T_MAX]
201-280PySlice_AdjustIndicesNormalize start/stop to [0, length], return step count
281-330slice_indicesPython-level slice.indices(n) method
331-360slice_hashHash (start, stop, step) tuple hash
361-400Type object, __reduce__PySlice_Type slots and pickle support

Reading

PySliceObject and PySlice_Unpack

PySliceObject stores start, stop, and step as raw Python objects. None is valid and means "use the default". PySlice_Unpack converts them to C Py_ssize_t values with explicit clamping so that callers never have to guard against overflow:

int
PySlice_Unpack(PyObject *_r,
Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step)
{
/* step */
if (r->step == Py_None) {
*step = 1;
} else {
*step = _PyEval_SliceIndex(r->step);
if (*step == 0) { PyErr_SetString(...); return -1; }
/* clamp to avoid overflow in AdjustIndices */
if (*step < -PY_SSIZE_T_MAX) *step = -PY_SSIZE_T_MAX;
}
/* start */
if (r->start == Py_None) {
*start = (*step < 0) ? PY_SSIZE_T_MAX : 0;
} else {
*start = _PyEval_SliceIndex(r->start);
/* no further clamping needed; AdjustIndices handles bounds */
}
/* stop: symmetric with start */
...
return 0;
}

The only hard clamp is step against -PY_SSIZE_T_MAX (not MIN) to prevent -step from overflowing during length calculations in AdjustIndices.

PySlice_AdjustIndices

After Unpack yields raw signed integers, AdjustIndices normalises them against the actual sequence length and returns the number of elements the slice covers:

Py_ssize_t
PySlice_AdjustIndices(Py_ssize_t length,
Py_ssize_t *start, Py_ssize_t *stop,
Py_ssize_t step)
{
if (*start < 0) { *start += length; if (*start < 0) *start = (step<0)?-1:0; }
else if (*start >= length) *start = (step<0) ? length-1 : length;

if (*stop < 0) { *stop += length; if (*stop < 0) *stop = (step<0)?-1:0; }
else if (*stop >= length) *stop = (step<0) ? length-1 : length;

if (step < 0) {
if (*stop < *start) return (*start - *stop - 1) / (-step) + 1;
} else {
if (*start < *stop) return (*stop - *start - 1) / step + 1;
}
return 0;
}

The division (range - 1) / abs(step) + 1 is the ceiling-division form that counts items correctly for any step magnitude without floating-point.

Ellipsis singleton and slice_hash

Py_Ellipsis is a true singleton: PyEllipsis_Type has no fields beyond the base object header. Equality is always identity. Repr is the string "Ellipsis" and the literal ... in Python source maps directly to this object.

slice_hash delegates to PyObject_Hash on a temporary (start, stop, step) tuple. Slices were unhashable before 3.12. The 3.12 change made them hashable only when all three components are hashable, which required no struct change, just wiring up the tp_hash slot:

static Py_hash_t
slice_hash(PySliceObject *v)
{
PyObject *t = PyTuple_Pack(3, v->start, v->stop, v->step);
if (t == NULL) return -1;
Py_hash_t h = PyObject_Hash(t);
Py_DECREF(t);
return h;
}

gopy notes

  • objects/slice.go mirrors PySliceObject as Slice{Start, Stop, Step Object}. Unpack and AdjustIndices are ported as methods (*Slice).Unpack(length int64) returning (start, stop, step, count int64, err error) in one call, combining both CPython functions to avoid the intermediate struct.
  • The Ellipsis singleton is objects.Ellipsis, a package-level *EllipsisObject initialised in init(). Identity comparison uses pointer equality, matching CPython behaviour.
  • slice_hash is ported by hashing a [3]Object array via the standard objects.TupleHash helper rather than allocating a real tuple.
  • 3.14 change: _PyEval_SliceIndex was made static inline and moved into the header Include/internal/pycore_sliceobject.h. The gopy equivalent (sliceIndex in objects/slice.go) was already a private function, so no public API change is required.