How Python deque Achieves O(1) Appends and Pops

Python's collections.deque (double-ended queue) achieves \(O(1)\) time complexity for appends and pops from both ends by using an internal doubly linked list of fixed-size memory blocks rather than a single contiguous dynamic array. While a standard Python list requires \(O(n)\) time to shift every element when prepending, deque avoids bulk memory reallocation and shifting altogether. This article explains the underlying CPython data structure, how the block system handles bidirectional mutations, and why it outperforms traditional lists and pure linked lists for queue operations.

The Underlying Architecture: A Doubly Linked List of Blocks

Unlike a standard Python list, which is a flat, contiguous dynamic array (like a C++ std::vector), collections.deque is implemented in CPython as a doubly linked list of fixed-size arrays. In the CPython source code (Modules/_collectionsmodule.c), each node in this linked list is defined as a block.

Each block contains:

The deque object itself tracks the leftmost and rightmost blocks, along with indices indicating the current boundary positions within those edge blocks.

How Appends and Pops Stay O(1)

Because the container maintains direct pointers to both the leftmost and rightmost blocks, operations on either end do not require traversing the entire collection.

Appending to the Ends (append and appendleft)

When adding an item to either end:

  1. The deque checks whether the current edge block (left or right) has free slots.
  2. If space is available, the element pointer is stored directly in the block's internal array at the next available index, taking strictly \(O(1)\) time.
  3. If the edge block is full, a new fixed-size block is allocated, connected via pointers to the previous edge block, and updated as the new edge. This allocation is also an \(O(1)\) operation because the block size is fixed and does not scale with the total number of items in the deque.

Popping from the Ends (pop and popleft)

When removing an item from either end:

  1. The element pointer is read and cleared from the edge block at the current boundary index.
  2. If the block still contains remaining elements, only the boundary index is adjusted (\(O(1)\)).
  3. If the block becomes entirely empty, it is unlinked from the chain, freed from memory, and the adjacent block becomes the new edge block (\(O(1)\)).

Why deque Outperforms Lists and Simple Linked Lists

In a standard Python list, inserting or removing an item at the beginning (insert(0, val) or pop(0)) requires shifting every subsequent element in memory to the left or right, causing \(O(n)\) performance. deque eliminates shifting because new elements simply fill the remaining slots of an existing block or trigger the creation of a new linked block.

At the same time, deque outperforms a naive, single-element doubly linked list by significantly reducing memory overhead and improving CPU cache locality:

The Trade-Off: Slow Random Access

While deque guarantees true \(O(1)\) operations at both ends, it sacrifices fast random access. Accessing an element by index in a standard list is an instant \(O(1)\) offset calculation (base_address + index * pointer_size). In a deque, accessing d[n] requires calculating which block contains the index and traversing through the linked blocks from the nearest end, resulting in \(O(n)\) worst-case lookup performance for items located in the middle of the collection.