When list is not the right container

array, deque, memoryview, and generators for tighter memory and I/O

Lists are flexible, and that flexibility has a cost. For numeric data, `array.array` stores raw C doubles directly, skipping per-element `PyObject` overhead entirely. For FIFO queues, `collections.deque` provides O(1) appends and pops from both ends, while `list.pop(0)` is O(n) because every remaining reference must shift in the contiguous array. For binary data and buffer protocols, `memoryview` provides zero-copy slices over underlying buffers. Generators produce values on demand instead of holding everything in memory at once. Each alternative trades generality for performance in a specific dimension. This page covers `array`, `deque`, `memoryview`, and generators with memory measurements and use-case guidance. <a href="/memory-container-comparison">Compare all container types side by side</a>. <a href="/memory-iterables">Understand container vs streaming iterables</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

The storage model is the optimization.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
4.2When list is not the right container

array, deque, memoryview, and generators for tighter memory and I/O

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 deque
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Job:
job_id: str
priority: int
def drain(queue: deque[Job]) -> list[str]:
finished: list[str] = []
while queue:
finished.append(queue.popleft().job_id)
return finished
print(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 array
from dataclasses import dataclass
from sys import getsizeof
@dataclass(frozen=True, slots=True)
class Frame:
header: bytes
payload: bytes
def 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 deque
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Task:
name: str
def drain_list(tasks: list[Task]) -> list[str]:
output: list[str] = []
while tasks:
output.append(tasks.pop(0).name)
return output
def 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.

Further reading

  • Official docs: deque
  • Official docs: array
  • Official docs: memoryview
  • CPython source: list implementation
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

The storage model is the optimization.

RELATED GUIDES
NEXT CHECKS
Contribute