In CPython, a list stores references to objects. Each integer also occupies memory for its value and runtime bookkeeping. For a large numeric workload, those allocations can dominate the size of the underlying data.
Measure the right boundary #
sys.getsizeof reports the size of an object itself, not the full graph of objects it references. Comparing a list to an array with that one number can hide most of the cost.
import sys
from array import array
values = list(range(10_000))
packed = array('q', values)
print(sys.getsizeof(values)) # Container, excluding its integers.
print(sys.getsizeof(packed)) # Includes the contiguous buffer.For this example, estimate the list’s retained size by adding the container size to the size of each distinct referenced integer. For a general object graph, track visited identities: shared references must be counted once, and cycles must terminate. Small-integer caching and allocator overhead also affect what a process-level measurement reports.
array('q') stores signed integers in a contiguous buffer. It has a fixed numeric range, unlike Python’s arbitrary-precision integers. Check that range before changing the representation.
Let the access pattern decide #
Packed storage is useful when operations can stay in a compact representation. Converting back and forth in a hot loop can spend the memory savings on CPU time and additional allocations.
Complexity has a maintenance cost #
An optimization deserves a clear workload and a repeatable measurement. Keep the compact representation only when the measured savings justify the range constraints and conversion logic. Record the workload with the benchmark so the decision can be revisited when access patterns change.