Skip to main content

Objects/listobject.c (sort and resize)

Objects/listobject.c is the largest file in Objects/ after unicodeobject.c. It covers the mutable sequence type: dynamic resizing with overallocation, the timsort implementation used by list.sort() and the sorted() built-in, in-place reversal, slicing, and the mutation-detecting iterator. The sort alone accounts for roughly 1400 of the 2700 lines.

Map​

LinesSymbolRole
1–80PyListObject struct, ob_item, allocatedHeader plus pointer to heap-allocated item array; allocated >= ob_size
81–200list_resizeDynamic resize: realloc with overallocation when growing, shrink when ob_size < allocated/2
201–350app1, listappend, listinsertappend and insert paths; both call list_resize
351–500list_slice, list_ass_sliceSlice read (returns new list) and slice assignment (in-place mutation)
501–650list_reverseIn-place pointer swap from both ends toward the middle
651–800listiter, listiter_nextIterator with it_seq/it_index and ob_size mutation guard
801–2700listsort subsystemFull timsort: merge_lo/hi, merge_at, gallop_left/right, run detection, pending-run stack

Reading​

list_resize: the overallocation formula​

When the requested new size is larger than allocated, list_resize calls PyMem_Realloc with a larger buffer. The target size is chosen so that repeated single-element appends amortize to O(1): the formula grows by roughly 12.5% plus a small constant.

// CPython: Objects/listobject.c:97 list_resize
static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{
Py_ssize_t allocated = self->allocated;

if (allocated >= newsize && newsize >= (allocated >> 1)) {
/* within the current buffer; just update ob_size */
Py_SET_SIZE(self, newsize);
return 0;
}

/* new_allocated = newsize + newsize/8 + (newsize < 9 ? 3 : 6) */
Py_ssize_t new_allocated = (size_t)newsize + (newsize >> 3) +
(newsize < 9 ? 3 : 6);
...
PyObject **items = self->ob_item;
if (new_allocated <= (PY_SSIZE_T_MAX / sizeof(PyObject *)))
PyMem_Realloc(items, new_allocated * sizeof(PyObject *));
...
}

The shrink guard (newsize >= allocated >> 1) prevents oscillation: if the list is between 50% and 100% full it keeps the existing buffer rather than reallocating immediately.

list_reverse: in-place pointer swap​

list.reverse() runs in O(n) time with O(1) extra space by swapping pairs of pointers from both ends inward. No new list object is allocated.

// CPython: Objects/listobject.c:520 listreverse
static int
list_reverse_impl(PyListObject *self)
{
PyObject **lo = self->ob_item;
PyObject **hi = self->ob_item + Py_SIZE(self) - 1;
while (lo < hi) {
PyObject *t = *lo;
*lo = *hi;
*hi = t;
lo++;
hi--;
}
return 0;
}

Because list_reverse operates directly on ob_item pointers it does not alter reference counts; each pointer is moved but the pointed-to object is unchanged.

listiter: mutation guard via ob_size snapshot​

The list iterator records the list's ob_size at creation time inside it_seq. Each call to listiter_next checks whether the list length matches the recorded snapshot; a mismatch raises RuntimeError: list changed size during iteration.

// CPython: Objects/listobject.c:680 listiter_next
static PyObject *
listiter_next(listiterobject *it)
{
PyListObject *seq = it->it_seq;
if (seq == NULL)
return NULL;

Py_ssize_t index = it->it_index;
if (index < PyList_GET_SIZE(seq)) {
PyObject *item = PyList_GET_ITEM(seq, index);
++it->it_index;
Py_INCREF(item);
return item;
}
it->it_seq = NULL;
Py_DECREF(seq);
return NULL;
}

The full mutation check is in the ITERNEXT wrapper generated by the Argument Clinic machinery; the inner function shown here focuses on the hot path.

listsort timsort: run detection and galloping​

The sort subsystem occupies lines 801 to 2700. Timsort finds existing sorted runs in the input (ascending or descending, the latter reversed in place), then merges them using a pending-run stack. When one side of a merge wins many consecutive comparisons the code switches to gallop mode, which uses binary search to find the changeover point and copies a bulk prefix before resuming element-by-element comparison.

// CPython: Objects/listobject.c:1120 merge_at
static int
merge_at(MergeState *ms, Py_ssize_t i)
{
sortslice ssa = ms->pending[i].base;
Py_ssize_t na = ms->pending[i].len;
sortslice ssb = ms->pending[i+1].base;
Py_ssize_t nb = ms->pending[i+1].len;

ms->pending[i].len = na + nb;
if (i == ms->n - 3)
ms->pending[i+1] = ms->pending[i+2];
--ms->n;

/* find where b[0] sits in a; a[k] < b[0] elements can be ignored */
k = gallop_right(*ssb.keys, ssa.keys, na, 0);
...
/* merge remaining [k:na] of a with all of b */
if (na - k <= nb)
return merge_lo(ms, ssa, na - k, ssb, nb);
else
return merge_hi(ms, ssa, na - k, ssb, nb);
}

merge_lo copies the shorter run into a temporary buffer and merges forward; merge_hi copies the shorter run and merges backward. Choosing the direction keeps the temporary allocation small.

// CPython: Objects/listobject.c:980 gallop_right
static Py_ssize_t
gallop_right(PyObject *key, PyObject **a, Py_ssize_t n, Py_ssize_t hint)
{
/* exponential search from hint outward, then binary search */
Py_ssize_t ofs = 1, lastofs = 0;
...
while (ofs < n) {
int k = ISLT(key, a[hint + ofs], compare);
if (k < 0) return -1;
if (k) break;
lastofs = ofs;
ofs = (ofs << 1) + 1;
if (ofs <= 0) ofs = n; /* overflow guard */
}
/* binary search in (hint+lastofs, hint+ofs] */
...
}

The ISLT macro abstracts the comparison function pointer so that timsort works for both the default key-less case and the key= argument case without branching in the inner loop.

gopy notes​

Status: not yet ported.

Planned package path: objects/list.go for the list type, resize, append, and slicing; objects/list_sort.go for the full timsort subsystem; objects/list_iter.go for the iterator.

Key porting decisions to resolve:

  • list_resize's overallocation formula must be replicated exactly (including the newsize < 9 ? 3 : 6 branch) so that capacity growth matches CPython's behavior for tests that inspect sys.getsizeof trends.
  • The timsort in objects/list_sort.go will be a direct port of merge_lo, merge_hi, merge_at, and gallop_left/right rather than delegating to Go's slices.SortFunc. Using Go's sort would not replicate CPython's stable-sort guarantee on already-sorted inputs or the gallop thresholds that affect performance-sensitive benchmarks.
  • The mutation guard in listiter_next maps to a version counter on the Go list struct. Each mutating method increments the counter; the iterator saves the counter at creation and checks it on each __next__ call.
  • PyList_GET_ITEM is an unchecked index; the gopy equivalent will be an inline method that performs no bounds check in release builds, matching the C macro semantics.