Lists are a good default for mutable ordered references. They are a poor default for FIFO queues, zero-copy byte windows, and dense homogeneous numeric columns.
Core answer
Use deque for work queues, array.array for packed numeric values, memoryview for buffer slices, and generators when one-pass production is enough.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from collections import dequefrom dataclasses import dataclass@dataclass(frozen=True, slots=True)class Job: job_id: str priority: intdef drain(queue: deque[Job]) -> list[str]: finished: list[str] = [] while queue: finished.append(queue.popleft().job_id) return finishedprint(drain(deque([Job("A", 1), Job("B", 2)])))Why this design exists
The list API pays for flexible indexed mutation and reference storage. Specialized standard-library containers narrow that promise to make a dominant operation cheaper or denser.
Mechanics and CPython internals
list.pop(0) shifts trailing references, so it is O(n). deque keeps block-oriented storage for efficient end operations. array.array stores primitive values inline by typecode. memoryview exposes the buffer protocol, so slices can view underlying bytes instead of allocating a new bytes object.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from array import arrayfrom dataclasses import dataclassfrom sys import getsizeof@dataclass(frozen=True, slots=True)class Frame: header: bytes payload: bytesdef patch_header(raw: bytearray) -> Frame: view = memoryview(raw) view[:4] = b"SYNC" return Frame(bytes(view[:4]), bytes(view[4:]))packed = array("d", [1.25, 2.50, 3.75])print(getsizeof([1.25, 2.50, 3.75]), getsizeof(packed))print(patch_header(bytearray(b"FAILpayload")))Complexity and tradeoffs
The gain follows the bottleneck. End operations on deque are O(1) while random indexing is not its strength. Packed arrays save memory but restrict element types. Views avoid copies but share mutability and lifetime with the underlying buffer.
Idiomatic patterns and refactoring
Refactor queue code away from front-popping lists once queue size matters.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from collections import dequefrom dataclasses import dataclass@dataclass(frozen=True, slots=True)class Task: name: strdef drain_list(tasks: list[Task]) -> list[str]: output: list[str] = [] while tasks: output.append(tasks.pop(0).name) return outputdef drain_deque(tasks: list[Task]) -> list[str]: queue = deque(tasks) return [queue.popleft().name for _ in range(len(queue))]print(drain_list([Task("one"), Task("two")]))print(drain_deque([Task("one"), Task("two")]))Common mistakes and edge cases
Do not replace list with deque when random indexed access is dominant. Do not treat a memoryview as an owning copy. Do not expect an array to hold mixed Python objects.
When to use / When NOT to use
Use list alternatives when storage model or dominant operation changes the cost profile materially.
Do not make a specialized container the default before the semantics and measurements justify its narrower contract.