Back to Blog
Python

python deque vs list: When to Use Each

python deque vs list: Compare Python's deque and list for append, pop, and indexing. Learn when each data structure is the right choice for queues, stacks, and random...

dequelistperformancedata structurescollectionsqueue
Illustration comparing Python deque and list data structures, showing a two-ended queue and a contiguous array with an index pointer.

When you need a mutable sequence in Python, list is usually the first tool that comes to mind. But the collections.deque class exists for a specific reason: it makes appending and popping from both ends efficient. The python deque vs list decision comes down to which operations dominate your workload, and understanding the underlying memory and access patterns of each structure.

What deque and list Have in Common

Both deque and list are mutable sequences that support iteration, indexing, slicing (though deque slicing is not supported directly), and common methods like len(), in, and count(). They both implement the sequence protocol, so you can pass either to functions that expect an iterable. For basic operations like iterating over elements or checking membership, the difference is negligible.

However, the internal storage differs fundamentally. A list is a dynamic array: a contiguous block of memory with a pointer to each element. A deque is a doubly-linked list of fixed-size blocks, where each block holds a small array of elements. This design gives deque its signature ability to grow and shrink from both ends without moving other elements.

Appending and Popping from the Right End

For appending to the end, both list.append() and deque.append() are amortized O(1). A list occasionally reallocates its underlying array, copying all references to a larger block, but the amortized cost remains constant. A deque, because it allocates new blocks as needed, also achieves O(1) per append, but with a slightly higher constant factor due to block management.

Popping from the right end is likewise O(1) for both. list.pop() removes the last element and shrinks the array when it becomes too sparse, while deque.pop() removes from the last block and frees it when empty. For a stack (LIFO) where you only push and pop from one end, a list is usually the better choice because it has lower overhead and better cache locality.

The Critical Difference: Appending and Popping from the Left

This is where the two structures diverge sharply. list.insert(0, item) and list.pop(0) are O(n) because every existing element must be shifted one position. For a list of one million elements, inserting at the front copies all references. In contrast, deque.appendleft() and deque.popleft() are O(1) because the deque simply links a new block at the front or removes the first block.

from collections import deque # List: O(n) for front insertion lst = [1, 2, 3] lst.insert(0, 0) # shifts all elements # Deque: O(1) for front insertion dq = deque([1, 2, 3]) dq.appendleft(0) # no shifting

If your algorithm repeatedly adds and removes from both ends—such as a sliding window, a double-ended queue, or a breadth-first search frontier—deque is the correct choice. Using a list for these operations would turn an O(1) algorithm into O(n) per operation, which can degrade a linear-time solution into quadratic.

Inserting and Deleting in the Middle

Neither structure is optimized for arbitrary middle insertion or deletion. list.insert(i, item) and list.pop(i) are O(n) because elements after the index must shift. deque also requires O(n) for middle operations because it must traverse the blocks to find the position, and then shift elements within the block. However, the constant factor is typically worse for deque due to the linked-list traversal and block boundaries.

# Both are O(n) for middle operations lst = [1, 2, 3, 4, 5] lst.insert(2, 99) # shifts 3,4,5 dq = deque([1, 2, 3, 4, 5]) dq.insert(2, 99) # traverses to index 2, then shifts within block

If your workload involves frequent insertions or deletions at arbitrary positions, neither deque nor list is ideal. Consider a balanced tree or a blist-style structure, though those are not part of the standard library. For occasional middle operations on a small sequence, the difference is negligible.

Indexing and Random Access

list provides O(1) random access because elements are stored contiguously and the address is computed directly from the index. deque also supports indexing in O(1) on average, but it must traverse the block chain to find the correct block, then index within that block. This adds a constant overhead that becomes noticeable for large deques and frequent random access.

lst = list(range(1000000)) dq = deque(range(1000000)) # List indexing is a direct pointer arithmetic x = lst[500000] # Deque indexing traverses blocks to find the right one y = dq[500000]

For algorithms that rely heavily on random access, such as binary search or sorting, a list is the clear winner. Sorting a deque is also possible, but deque does not have a sort() method; you must convert it to a list first, sort, and convert back. The conversion itself is O(n), which is acceptable if sorting is infrequent.

Memory and Cache Behavior

A list stores references in a contiguous array, which makes iteration and access cache-friendly. A deque stores references in separate blocks, so iterating over a large deque may incur cache misses when jumping between blocks. The block size in CPython is 64 elements, so for small deques the overhead is minimal, but for large ones the memory layout can affect performance.

Memory usage also differs. A list overallocates its array to amortize appends, so it may use more memory than the number of elements. A deque allocates blocks on demand, but each block has a fixed overhead. For many small elements, a deque may use more memory per element than a list due to block metadata. If memory footprint is critical and you only need stack-like behavior, a list is usually more compact.

Practical Decision Guide

Use deque when you need O(1) appends and pops from both ends. Typical scenarios include:

  • Implementing a queue (FIFO) where you popleft() and append().
  • Sliding window algorithms that add to the right and remove from the left.
  • BFS traversal where you maintain a frontier.
  • Keeping a bounded history of recent items, using maxlen to automatically discard old items.

Use list when you need:

  • Random access by index, especially for algorithms like binary search.
  • Sorting, because list.sort() is in-place and efficient.
  • Slicing, which returns a new list and is not available for deque.
  • Stack behavior (LIFO) where you only append and pop from the same end.
  • Interoperability with functions that expect a list, such as json.dumps() or random.choice().
from collections import deque # Queue using deque q = deque() q.append("task1") q.append("task2") first = q.popleft() # "task1" # Bounded history dq = deque(maxlen=3) for i in range(5): dq.append(i) # dq is now deque([2, 3, 4])

The maxlen parameter is a unique feature of deque that automatically discards items from the opposite end when the deque is full. This is useful for keeping a rolling window of recent events without manual checks.

Performance Measurement and Big-O vs Constants

When comparing python deque vs list, it's tempting to rely solely on Big-O notation. While Big-O tells you how an operation scales, it doesn't tell you the constant factor. For small collections, a list's cache-friendly contiguous storage often outperforms a deque even for front operations, because the overhead of block management outweighs the O(n) shift. Conversely, for large collections, the O(n) shift becomes dominant and deque wins clearly.

A practical approach is to profile with realistic data sizes. Use the timeit module to measure your specific workload. For example, compare list.pop(0) and deque.popleft() on a list of 100,000 elements:

from timeit import timeit from collections import deque lst = list(range(100000)) dq = deque(range(100000)) list_time = timeit(lambda: lst.pop(0), number=1000) deque_time = timeit(lambda: dq.popleft(), number=1000) print(f"list pop(0): {list_time:.4f}s") print(f"deque popleft: {deque_time:.4f}s")

The exact numbers depend on your hardware and Python version, but you'll typically see deque's popleft take microseconds while list's pop(0) takes milliseconds. For small collections, the difference may be negligible, so don't over-optimize prematurely.

Compatibility and Edge Cases

deque is part of the collections module, which is always available in CPython. However, deque does not support slicing, and it doesn't have __getitem__ with slice objects. If you need to slice, convert to a list first. Also, deque is not a subclass of list, so functions that explicitly check isinstance(x, list) will reject it. In most contexts, duck typing makes this irrelevant, but be aware if you're using type annotations.

Another subtlety: deque is thread-safe for appends and pops from opposite ends, but not for multiple threads modifying the same end. The documentation states that append() and popleft() are atomic, making it useful for producer-consumer patterns without extra locks. A list has no such guarantee; concurrent modifications require external synchronization.

When you need to serialize a deque, json.dumps() cannot handle it directly because it's not a list. Convert to a list first: list(dq). Similarly, pickle works with deque, but the serialized format is larger than a list's. For most applications, these are minor considerations, but they can affect production code that relies on standard serialization.

Ultimately, the choice between deque and list should be driven by the dominant operations in your algorithm. If you find yourself using insert(0, ...) or pop(0) on a list, switch to a deque. If you're doing heavy random access or sorting, stick with a list. Understanding the internal mechanics of each structure lets you make that decision with confidence rather than guessing.

python deque vs list: When to Use Each | RYUSLOG DEV