You reach for list by reflex every time you need an ordered collection. And that is usually fine — until you store a million floats and wonder why memory is exploding, or you pop from the left and wonder why it is so slow. list is a generalist, not a specialist.
Think of list like a universal toolbox drawer: great for mixed contents, easy to reorganize. But when you need to store 10,000 screws, a dedicated organizer (array.array) uses a fraction of the space. When you need fast access from both ends, a bucket on a rope (collections.deque) beats pulling every tool out from the front.
Choose the container by representation and mutation pattern:
list: heterogeneous Python objects, random access, right-end appendarray.array: homogeneous C values packed contiguouslycollections.deque: fast append/pop at both endsmemoryview: zero-copy window over binary buffers
# [CURRENT - 3.10-3.14] Works on Python 3.xfrom collections import dequeevents = deque(maxlen=3)for event in ["a", "b", "c", "d"]: events.append(event)print(list(events)) # ['b', 'c', 'd']A list stores references to Python objects. For numeric data, that means one pointer per element plus one full Python number object per element. An array.array("d") stores raw C double values directly, with no separate Python float object per element. On mainstream 64-bit CPython builds that is typically 8 bytes per element, but the exact width comes from the platform C type.
Measured locally on CPython 3.12.3, 64-bit Linux:
sys.getsizeof([1.0] * 1000)->8056bytes for the list container onlysys.getsizeof((1.0,) * 1000)->8040bytes for the tuple container onlysys.getsizeof(array("d", [1.0] * 1000))->8080bytes total for the packed doubles
Those list and tuple measurements do not include the float objects referenced by the elements. The array measurement does include the raw numeric payload. That is why the real memory gap becomes dramatic for numeric workloads.
# [CURRENT - 3.10-3.14] Works on Python 3.x# Example byte counts below were measured on CPython 3.12.3, 64-bit Linux.import sysfrom array import arrayn = 10values = [float(i) for i in range(n)]list_total = sys.getsizeof(values) + sum(sys.getsizeof(v) for v in values)values_t = tuple(float(i) for i in range(n))tuple_total = sys.getsizeof(values_t) + sum(sys.getsizeof(v) for v in values_t)array_total = sys.getsizeof(array("d", [float(i) for i in range(n)]))print(list_total) # 376print(tuple_total) # 360print(array_total) # 160This is the same container-vs-flat distinction discussed in : lists and tuples are object containers, arrays are packed value storage.
list.append() and list.pop() at the right edge are excellent. list.pop(0) and list.insert(0, value) are not: CPython must shift all remaining references because the underlying representation is a contiguous array of pointers.
deque is designed for that queue shape. The official docs describe appends and pops from either side as approximately O(1), while list left-edge mutation incurs O(n) memory movement.
# [CURRENT - 3.10-3.14] Works on Python 3.xqueue = [1, 2, 3]first = queue.pop(0)print(first, queue)# [CURRENT - 3.10-3.14] Works on Python 3.xfrom collections import dequequeue = deque([1, 2, 3])first = queue.popleft()print(first, queue)Local timeit measurements on this machine for 1000 operations over a 10000-element container showed roughly:
list.pop(0): about1.0e-3secondsdeque.popleft(): about2.0e-5seconds
Do not treat those exact numbers as portable. The stable lesson is the algorithmic one: queue-like left-edge mutation is a deque workload, not a list workload.
memoryview exposes the buffer protocol. It lets you slice and mutate bytes-like storage without allocating a copied sub-buffer.
# [CURRENT - 3.10-3.14] Works on Python 3.xbuffer = bytearray(b"abcdefgh")view = memoryview(buffer)view[2:5] = b"XYZ"print(buffer) # bytearray(b'abXYZfgh')The view object itself is small and does not own the bytes. It holds metadata describing how to interpret another object's memory.
Measured locally on CPython 3.12.3:
sys.getsizeof(bytes(1000))->1033sys.getsizeof(bytearray(1000))->1057sys.getsizeof(memoryview(bytearray(1000)))->184
That 184 bytes is the view wrapper, not a second 1000-byte copy.
array.array, collections.deque, and memoryview are stable Python 3 facilities. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.
The exact byte counts in this guide are CPython 3.12.3, 64-bit measurements taken locally. They are useful for understanding cost shape, not for writing byte-exact production invariants.
deque is not a universally faster list. Middle indexing is slower, slicing is not supported, and many APIs still expect ordinary sequences.
array.array only works when every element fits the chosen type code. It is ideal for dense numeric buffers but not for mixed Python objects.
memoryview is for bytes-like storage, not arbitrary object containers. You cannot create a memory view over a list of Python integers.
If the data is one-pass, avoid materializing any container at all. A generator can beat every structure in memory usage because it stores control state instead of the full result.
Use ──────────────────────────────────────────────
listfor general-purpose mutable sequencesdequefor queues, recent-history buffers, and producer/consumer edgesarray.arrayfor compact numeric buffers and binary file I/Omemoryviewfor packet parsing, protocol frames, and binary slicing without copies
# [CURRENT - 3.10-3.14] Works on Python 3.xfrom array import arraydef save_temperatures(readings, path): payload = array("d", readings) with open(path, "wb") as fp: payload.tofile(fp)When the goal is laziness rather than compact storage, use iterator pipelines from .