CPython's slice Implementation in C: Three Objects and a Bunch of Clamping
slice is a GC-tracked object holding start, stop and step. Here's how CPython implements it in C: the freelist, PySlice_Unpack, PySlice_AdjustIndices, and the overflow-free _PySlice_GetLongIndices.
TL;DR:
sliceis a tiny GC-tracked object holding three arbitrary Python objects —start,stop,step— withNonestanding for “omitted”. Turning those into real indices happens later, inPySlice_Unpack(objects →Py_ssize_t) andPySlice_AdjustIndices(clamping to the sequence length).
Python’s slice is the object produced by a[0:10:2] and by slice(0, 10, 2). In CPython it lives in Objects/sliceobject.c as a small GC-tracked struct holding three arbitrary Python objects, where None (Py_None) stands for “omitted”. The same file also owns the immortal Ellipsis singleton. Let’s trace the implementation through the 3.15 source.
The object: PySliceObject
The public (limited-API) layout is defined in Include/cpython/sliceobject.h:
// Include/cpython/sliceobject.h
typedef struct {
PyObject_HEAD
PyObject *start, *stop, *step; /* not NULL */
} PySliceObject;Three invariants follow from this:
- The three fields are never NULL. An omitted bound is stored as
Py_None, which is whyslice(5)producesslice(None, 5, None). - They are arbitrary Python objects. They need not be integers; anything with
__index__is accepted later, when the slice is actually applied. PySlice_Check(op)is an exact type check (Py_IS_TYPE((op), &PySlice_Type)), so a subclass ofsliceis not a slice for C-level fast paths.
PySlice_Type.tp_basicsize is sizeof(PySliceObject) and tp_itemsize is 0, so the object is fixed-size. It’s GC-tracked (Py_TPFLAGS_HAVE_GC) because it owns three references.
Construction and the freelist
slice_new (the tp_new) accepts 1 to 3 positional arguments and swaps start/stop to mirror range()’s calling convention:
// Objects/sliceobject.c
static PyObject *
slice_new(PyTypeObject *type, PyObject *args, PyObject *kw)
{
PyObject *start, *stop, *step;
start = stop = step = NULL;
if (!_PyArg_NoKeywords("slice", kw))
return NULL;
if (!PyArg_UnpackTuple(args, "slice", 1, 3, &start, &stop, &step))
return NULL;
/* This swapping of stop and start is to maintain similarity with range(). */
if (stop == NULL) {
stop = start;
start = NULL;
}
return PySlice_New(start, stop, step);
}PySlice_New normalizes NULLs to Py_None and delegates to _PyBuildSlice_Consume2, which pops from a tiny per-interpreter freelist before falling back to PyObject_GC_New:
// Objects/sliceobject.c
static PySliceObject *
_PyBuildSlice_Consume2(PyObject *start, PyObject *stop, PyObject *step)
{
assert(start != NULL && stop != NULL && step != NULL);
PySliceObject *obj = _Py_FREELIST_POP(PySliceObject, slices);
if (obj == NULL) {
obj = PyObject_GC_New(PySliceObject, &PySlice_Type);
if (obj == NULL) {
goto error;
}
}
obj->start = start;
obj->stop = stop;
obj->step = Py_NewRef(step);
_PyObject_GC_TRACK(obj);
return obj;
error:
Py_DECREF(start);
Py_DECREF(stop);
return NULL;
}Two things worth noting:
startandstopare consumed (their refcounts are taken over);stepis re-referenced viaPy_NewRef. That asymmetry is why the bytecodeBUILD_SLICEcallsPySlice_New(start_o, stop_o, step_o)with borrowed references, while_PyBuildSlice_ConsumeRefs(used bySTORE_SLICEand_STORE_SLICE) stealsstart/stopto avoid extra refcount churn.- The freelist holds at most
Py_slices_MAXFREELIST(1) entry, so it’s essentially a single-slot cache.slice_deallocreturns the object to it via_Py_FREELIST_FREE(slices, r, PyObject_GC_Del).
_PySlice_FromIndices is a convenience constructor used when C code already has Py_ssize_t bounds and wants a slice object:
// Objects/sliceobject.c
PyObject *
_PySlice_FromIndices(Py_ssize_t istart, Py_ssize_t istop)
{
PyObject *start, *end, *slice;
start = PyLong_FromSsize_t(istart);
...
slice = PySlice_New(start, end, NULL); /* step defaults to None -> 1 */
...
}BUILD_SLICE in the bytecode
The compiler emits BUILD_SLICE for subscript expressions like a[i:j:k]:
// Python/bytecodes.c
inst(BUILD_SLICE, (args[oparg] -- slice)) {
PyObject *start_o = PyStackRef_AsPyObjectBorrow(args[0]);
PyObject *stop_o = PyStackRef_AsPyObjectBorrow(args[1]);
PyObject *step_o = oparg == 3 ? PyStackRef_AsPyObjectBorrow(args[2]) : NULL;
PyObject *slice_o = PySlice_New(start_o, stop_o, step_o);
DECREF_INPUTS();
ERROR_IF(slice_o == NULL);
slice = PyStackRef_FromPyObjectStealMortal(slice_o);
}oparg is 2 or 3 depending on whether a step was given, and None is pushed for omitted bounds, so PySlice_New always receives non-NULL values.
Unpacking a slice: PySlice_Unpack
Before a slice can be applied, its three PyObject * fields must become Py_ssize_t values. PySlice_Unpack does that, treating None as a sentinel and clamping/boosting to PY_SSIZE_T_MIN/PY_SSIZE_T_MAX:
// Objects/sliceobject.c
int
PySlice_Unpack(PyObject *_r,
Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step)
{
PySliceObject *r = (PySliceObject*)_r;
if (r->step == Py_None) {
*step = 1;
} else {
if (!_PyEval_SliceIndex(r->step, step)) return -1;
if (*step == 0) {
PyErr_SetString(PyExc_ValueError, "slice step cannot be zero");
return -1;
}
/* Replace -PY_SSIZE_T_MAX-1 with -PY_SSIZE_T_MAX to guard against
later undefined behaviour from "step = -step" reversal. */
if (*step < -PY_SSIZE_T_MAX)
*step = -PY_SSIZE_T_MAX;
}
if (r->start == Py_None) {
*start = *step < 0 ? PY_SSIZE_T_MAX : 0;
} else {
if (!_PyEval_SliceIndex(r->start, start)) return -1;
}
if (r->stop == Py_None) {
*stop = *step < 0 ? PY_SSIZE_T_MIN : PY_SSIZE_T_MAX;
} else {
if (!_PyEval_SliceIndex(r->stop, stop)) return -1;
}
return 0;
}_PyEval_SliceIndex (in Python/ceval.c) is the bridge from “Python object” to “C integer”:
// Python/ceval.c
int
_PyEval_SliceIndex(PyObject *v, Py_ssize_t *pi)
{
if (Py_IsNone(v)) {
return 1; /* leaves *pi untouched */
}
return _PyEval_SliceIndexNotNone(v, pi);
}
int
_PyEval_SliceIndexNotNone(PyObject *v, Py_ssize_t *pi)
{
PyThreadState *tstate = _PyThreadState_GET();
Py_ssize_t x;
if (PyLong_CheckExact(v) && _PyLong_IsCompact((PyLongObject *)v)) {
*pi = _PyLong_CompactValue((PyLongObject *)v);
return 1;
}
if (_PyIndex_Check(v)) {
x = PyNumber_AsSsize_t(v, NULL); /* calls __index__ */
if (x == -1 && _PyErr_Occurred(tstate))
return 0;
} else {
_PyErr_SetString(tstate, PyExc_TypeError,
"slice indices must be integers or have an __index__ method");
return 0;
}
*pi = x;
return 1;
}So an omitted bound becomes the extreme sentinel (PY_SSIZE_T_MAX / PY_SSIZE_T_MIN), __index__ is honored for non-None values, and huge values are silently clamped to the Py_ssize_t range. None-handling for step defaults to 1.
The older PySlice_GetIndices (deprecated in 3.7) is a simpler variant that requires PyLong bounds and returns -1 on out-of-range. New code should use PySlice_Unpack + PySlice_AdjustIndices.
Adjusting indices: PySlice_AdjustIndices
After unpacking, the raw start/stop (which may be negative, or the extreme sentinels) must be clamped to [0, length] — or the reverse range for a negative step. PySlice_AdjustIndices does this and returns the slice length:
// Objects/sliceobject.c
Py_ssize_t
PySlice_AdjustIndices(Py_ssize_t length,
Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t step)
{
assert(step != 0);
assert(step >= -PY_SSIZE_T_MAX);
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 logic, in three steps:
- Negative
start/stopare taken relative tolength(i += length), then clamped to the appropriate boundary (0/lengthfor forward steps,-1/length-1for backward steps). - Out-of-range positive values are clamped to the same boundaries.
- The returned count is the number of elements the slice covers, computed with integer division so a partial final step isn’t counted.
PySlice_GetIndicesEx is just the two steps glued together:
// Objects/sliceobject.c
int
PySlice_GetIndicesEx(PyObject *_r, Py_ssize_t length,
Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step,
Py_ssize_t *slicelength)
{
if (PySlice_Unpack(_r, start, stop, step) < 0)
return -1;
*slicelength = PySlice_AdjustIndices(length, start, stop, *step);
return 0;
}slice.indices(len): the Python-facing version
slice.indices(length) returns a tuple of fully-adjusted (start, stop, step) integers. It’s implemented by _PySlice_GetLongIndices, which works in Python int objects (not Py_ssize_t) so huge values never overflow and out-of-bounds indices are clipped consistently:
// Objects/sliceobject.c
int
_PySlice_GetLongIndices(PySliceObject *self, PyObject *length,
PyObject **start_ptr, PyObject **stop_ptr,
PyObject **step_ptr)
{
PyObject *start=NULL, *stop=NULL, *step=NULL;
PyObject *upper=NULL, *lower=NULL;
int step_is_negative, cmp_result;
/* Convert step to an integer; raise for zero step. */
if (self->step == Py_None) {
step = _PyLong_GetOne();
step_is_negative = 0;
} else {
step = evaluate_slice_index(self->step); /* calls __index__ */
...
step_is_negative = step_sign < 0;
}
/* Bounds depend on step direction. */
if (step_is_negative) {
lower = PyLong_FromLong(-1L);
upper = PyNumber_Add(length, lower); /* length - 1 */
} else {
lower = _PyLong_GetZero(); /* 0 */
upper = Py_NewRef(length); /* length */
}
/* Compute start: None -> upper/lower; else clamp to [lower, upper]. */
if (self->start == Py_None) {
start = Py_NewRef(step_is_negative ? upper : lower);
} else {
start = evaluate_slice_index(self->start);
if (_PyLong_IsNegative((PyLongObject *)start)) {
PyObject *tmp = PyNumber_Add(start, length); /* start += length */
Py_SETREF(start, tmp);
cmp_result = PyObject_RichCompareBool(start, lower, Py_LT);
if (cmp_result) Py_SETREF(start, Py_NewRef(lower));
} else {
cmp_result = PyObject_RichCompareBool(start, upper, Py_GT);
if (cmp_result) Py_SETREF(start, Py_NewRef(upper));
}
}
/* ... analogous block for stop ... */
*start_ptr = start; *stop_ptr = stop; *step_ptr = step;
...
}The returned start/stop/step are new references that the caller owns; slice.indices packs them into a tuple via Py_BuildValue("(NNN)", ...).
Worth internalizing: the Py_ssize_t path and the Python-int path implement the same semantics with different trade-offs. The C path is fast but clamps; the Python path is exact and overflow-free.
Equality, hashing, and repr
slice_reprproducesslice(%R, %R, %R)from the three fields.slice_richcompareshort-circuits identity (v == w) toTruefor==/<=/>=andFalseotherwise; otherwise it packs both triples into tuples and delegates toPyObject_RichCompare.slice_hashmixes the three field hashes with the same xxhash-style mixer used for tuples.slice_traversevisits all three fields for the GC.
Ellipsis
The same file defines the immortal Ellipsis singleton used for ...:
// Objects/sliceobject.c
PyObject _Py_EllipsisObject = _PyObject_HEAD_INIT(&PyEllipsis_Type);ellipsis_new rejects any arguments and returns Py_Ellipsis, and ellipsis_dealloc re-immortalizes the object as a guard. Py_Ellipsis resolves to this struct (or to the immortal constant in the limited API).
Memory layout summary
PySliceObject
+---------------------------+
| ob_refcnt / ob_tid |
+---------------------------+
| *ob_type (&PySlice_Type) |
+---------------------------+
| *start (never NULL) | -> Py_None or any PyObject
+---------------------------+
| *stop (never NULL) |
+---------------------------+
| *step (never NULL) |
+---------------------------+Fixed size (sizeof(PySliceObject)), GC-tracked, with a 1-slot freelist. The three fields are the only payload, which is why a slice is cheap to create and why slice equality reduces to tuple equality of (start, stop, step).
Key takeaways
PySliceObjectholds three non-NULLPyObject *fields (start,stop,step); omitted bounds arePy_None.- Construction goes through
_PyBuildSlice_Consume2, which reuses a 1-slot freelist and tracks the object with the GC. PySlice_Unpackconverts the three fields toPy_ssize_t, treatingNoneas extreme sentinels and honoring__index__via_PyEval_SliceIndex.PySlice_AdjustIndicesclamps negative/out-of-range bounds and returns the number of elements covered._PySlice_GetLongIndicesis the Python-int(overflow-free) variant used byslice.indices()andrangeslicing.- The
Ellipsissingleton lives in the same translation unit.
Further reading
Objects/sliceobject.c– type object, construction, unpacking, adjustment,indices, hashing, GC traversal.Include/cpython/sliceobject.h–PySliceObjectlayout and the limited-API prototypes.Include/sliceobject.h– public C API (PySlice_New,PySlice_GetIndices,PySlice_Unpack,PySlice_AdjustIndices).Python/ceval.c–_PyEval_SliceIndex/_PyEval_SliceIndexNotNone.Python/bytecodes.c–BUILD_SLICE.Lib/test/test_slice.py– reference implementation and edge cases.