Python's sequence zoo exists because different use cases have fundamentally different performance and memory requirements. A single "array" type would be either slow, memory-wasteful, or restrictive.
Flexibility ←──────────────────→ Efficiency
(any type, easy to use) (typed, memory-compact)
list ─────── tuple ─────── array.array ─────── numpy.ndarray
│ │ │ │
mutable immutable typed flat typed + BLAS
any type any type single C type vectorized ops
28MB/1M 20MB/1M 8MB/1M float 8MB/1M float
floats floats (+ SIMD)
The existence of array.array lets Python compete with C for memory layout without requiring NumPy.
memoryview exposes Python's buffer protocol — a C-level interface (Py_buffer struct) that allows objects to share memory without copying.
Buffer Protocol Participants:
bytearray, bytes, array.array, numpy.ndarray
┌─────────────────────┐
│ Raw Memory Block │ (actual bytes in C)
└────────┬────────────┘
│ exposes via buffer protocol
┌────────▼────────────┐
│ memoryview │ (Python view object)
└─────────────────────┘
│ │
slicing .cast()
(zero-copy) (reinterpret)
The buffer protocol is why socket.recv_into(buffer) can write directly into a bytearray without an intermediate allocation. It's why NumPy arrays can be passed to C extension functions with zero copying.
Java's ArrayList grows by 1.5x (some versions 2x). Python grows by approximately 1.125x (1/8 of current size).
Python's reasoning (from CPython commit history):
- Memory pressure: Python is used as a scripting language in memory-constrained environments. 2x growth means up to 50% wasted memory at any time.
- Amortized cost: As long as the growth is geometric (any constant factor > 1), amortized O(1) appends are guaranteed.
- Observation: Most Python lists are small (<100 elements). Over-allocating by 1/8 is sufficient to avoid frequent resizes.
The formula:
# From CPython listobject.c
new_allocated = (newsize + (newsize >> 3) + 6) & ~3
# For n=0: 6 → 8 (rounds to nearest multiple of 4)
# For n=10: 14+2 → 16
# For n=100: 112+2 → 112
# For n=1000: 1125+2 → 1128The & ~3 rounds up to the nearest multiple of 4 to align with common CPU cache line sizes.
Python uses Timsort (Tim Peters, 2002), which:
- Identifies "runs" (already-sorted sub-sequences)
- Merges runs with an optimized merge algorithm
- Falls back to binary insertion sort for very small runs
Complexity:
- Best case: O(n) — nearly-sorted data (natural runs detected)
- Average/worst case: O(n log n)
Why it beats pure mergesort in practice: Real-world data has structure. A log file sorted by timestamp is already mostly sorted — Timsort exploits this, while a theoretical sort ignores it.
Stability: Timsort is stable — equal elements maintain their relative order. This matters for multi-key sorts:
# Sort by department first, then by name within department
employees.sort(key=lambda e: e.name) # Primary sort by name (stable)
employees.sort(key=lambda e: e.dept) # Now sort by dept — names stay ordered within deptThe decision to make tuples immutable is a semantic constraint, not just a technical limitation.
Tuples communicate intent: "this group of values forms a single record; the structure should not change."
Memory benefit: Python can intern short tuples (cache them for reuse). Python 3.9+ interns tuples of small integers and other immutable types. (1, 2, 3) evaluated multiple times may refer to the same object.
Hash benefit: Tuples of hashable elements are themselves hashable — usable as dict keys or in sets. This is critical for:
- Multi-dimensional dictionaries:
lookup[(row, col)] = value - Memoization:
cache[tuple(args)] = result - Counting unique configurations
When you slice a list, Python creates a new list with new references to the same objects. This is a deliberate safety choice:
original = [1, 2, 3, 4, 5]
view = original[1:4] # New list: [2, 3, 4]
view[0] = 99 # Modifies view, not originalIf slices were views (like NumPy arrays), modifying view[0] would silently modify original[1]. For general Python lists containing heterogeneous objects, this would be extremely surprising and error-prone.
NumPy made the opposite choice: arr[1:4] returns a view, not a copy. This is appropriate for homogeneous numeric arrays where zero-copy slicing is critical for performance. NumPy compensates with explicit .copy() when you need independence.
memoryview also returns views: Appropriate because it's designed for binary data processing where zero-copy is the whole point.