Lists, Tuples, Slicing & Sequence Operations
Lists and tuples are CPython’s core ordered sequence containers. While lists are mutable dynamic pointer arrays, tuples are immutable fixed sequences. Understanding CPython’s dynamic array over-allocation formula, tuple freelist caching, slice memory copies, and zero-copy memoryview buffers is essential for performance engineering.
This chapter details PyListObject array resizing mechanics, tuple freelist allocation, slicing overhead, and memoryview buffers.
1. CPython List Architecture (PyListObject)
A Python list is not a linked list. It is an array of pointers to PyObject structs stored in contiguous C memory (PyObject** ob_item).
// CPython Internal Representation
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item; // Array of pointers to heap objects
Py_ssize_t allocated; // Total allocated slot capacity
} PyListObject;Growth Pattern & Over-Allocation Formula:
When you call list.append(), CPython doesn’t resize the underlying array by 1 slot. It over-allocates extra capacity to ensure $O(1)$ amortized append performance:
Over-allocation Formula:
new_allocated = N + (N >> 3) + (N < 9 ? 3 : 6)PyListObject Growth Pattern:
[ PyListObject Struct ]
├── ob_size: 4 (Current item count)
├── allocated: 8 (Total pointer array capacity)
└── ob_item (ptr) -> [ ptr0 | ptr1 | ptr2 | ptr3 | UNUSED | UNUSED | UNUSED | UNUSED ]When ob_size == allocated, CPython allocates a new, larger pointer array, copies the pointers over, and frees the old array.
2. Tuple Architecture & Freelist Caching (PyTupleObject)
Tuples (tuple) are immutable sequence containers. Because their size is fixed at creation, CPython stores tuple element pointers inline directly following the PyTupleObject header.
Freelist Caching:
To minimize malloc/free system calls for small tuples, CPython maintains an array of freelists for tuples of size 1 to 20:
- When a small tuple is garbage collected, CPython doesn’t return its C memory to the system allocator (
pymalloc). It puts the pre-allocated struct memory onto a freelist. - Creating a new small tuple grabs a pre-allocated block from the freelist, eliminating C heap allocation overhead.
3. Slicing Mechanics vs. Zero-Copy memoryview
Standard Slicing (a[start:stop]):
Slicing a list or tuple (my_list[2:5]) creates a completely new container object and copies the element pointers into the new array.
- Time Complexity:
O(K)whereK = stop - start. - Memory Overhead: Allocates a new
PyListObjectand copies $K$ pointer references.
Zero-Copy Buffers (memoryview):
When working with large binary buffers (e.g. video frames, socket streams), slicing bytes creates unnecessary memory copies. A memoryview exposes CPython’s internal Buffer Protocol (Py_buffer), allowing you to slice bytes without copying memory payload:
data = bytearray(b"X" * 10_000_000) # 10MB byte buffer
# Standard slice: Allocates a NEW 5MB byte object in memory!
chunk_copy = data[0:5_000_000]
# Memoryview slice: Zero-copy view! Shares original memory buffer.
view = memoryview(data)[0:5_000_000]
view[0] = ord('Y') # Modifies underlying data[0] in place!4. Production Trade-offs & Queue Selection
- Pop from Front: Calling
list.pop(0)orlist.insert(0, item)forces CPython to callmemmove, shifting all $N-1$ element pointers right or left. This takes $O(N)$ linear time. - High-Throughput Queues: Never use a
listas a FIFO queue. Usecollections.deque($O(1)$ head/tail operations via 64-element block arrays).