Skip to main content

How to Implement a Queue Using a List in Python

A queue is a linear data structure that follows the FIFO (First In, First Out) principle: the first element added is the first one to be removed. A real-world analogy is a line of customers at a ticket counter: the person who arrives first is served first.

In Python, you can implement a queue using a regular list (array-based approach) or a circular list to optimize performance.

This guide walks you through both implementations, explains their trade-offs, and shows you why the circular approach is almost always the better choice when working with fixed-size arrays.

Queue Implementation Using a Regular List

The most straightforward way to implement a queue is by using a fixed-size Python list. Elements are added at the rear and removed from the front by shifting all remaining elements one position to the left.

How It Works

  • Enqueue: Adds a new element at the current size index and increments the size counter.
  • Dequeue: Removes the front element by shifting every remaining element one position to the left, then decrements the size counter.
  • getFront: Returns the element at index 0 (the front) without removing it.
  • Display: Prints all elements currently in the queue.

Full Implementation

class Queue:
def __init__(self, q_size):
self.arr = [0] * q_size
self.size = 0
self.capacity = q_size
self.front = 0

def enqueue(self, x):
"""Add an element to the rear of the queue."""
if self.size == self.capacity:
print("Queue is full")
return

self.arr[self.size] = x
self.size += 1

def dequeue(self):
"""Remove the front element by shifting all others left."""
if self.size == 0:
print("Queue is empty")
return

# Shift all elements one position to the left
for i in range(1, self.size):
self.arr[i - 1] = self.arr[i]

self.size -= 1

def getFront(self):
"""Return the front element without removing it."""
if self.size == 0:
return -1
return self.arr[self.front]

def display(self):
"""Print all elements in the queue."""
for i in range(self.front, self.size):
print(self.arr[i], end=" ")
print()


q = Queue(4)

q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print(q.getFront())
q.dequeue()
q.enqueue(4)
q.display()

Output:

1
2 3 4

Why This Approach Is Inefficient

The critical problem with this implementation lies in the dequeue operation. Every time an element is removed, all remaining elements must be shifted one position to the left:

# This loop runs in O(n) time for every dequeue
for i in range(1, self.size):
self.arr[i - 1] = self.arr[i]
OperationTime Complexity
EnqueueO(1)
DequeueO(n), due to element shifting
getFrontO(1)
DisplayO(n)

Space Complexity: O(n), where n is the capacity of the queue.

O(n) dequeue is a performance bottleneck

For a proper queue, both enqueue and dequeue should run in O(1) time. Shifting elements on every removal makes this implementation unsuitable for performance-sensitive applications. The circular list approach below solves this problem entirely.

Queue Implementation Using a Circular List

A circular queue is an optimized version that eliminates the element-shifting overhead. Instead of moving elements after each dequeue, the front pointer simply advances forward. When either the front or rear pointer reaches the end of the underlying array, it wraps around to the beginning using the modulo operator (%).

This means the array positions are reused efficiently, and no space is wasted after dequeuing.

How It Works

The circular queue maintains three tracking variables:

  • front: The index of the first element in the queue.
  • size: The current number of elements (used to calculate the rear position).
  • cap: The maximum capacity of the queue.

The rear index is calculated dynamically as (front + size) % capacity rather than being stored separately. This avoids synchronization issues between front, rear, and size.

Enqueue logic:

  1. If the queue is full (size == capacity), reject the insertion.
  2. Calculate the rear index: rear = (front + size) % capacity.
  3. Place the new element at the rear index and increment size.

Dequeue logic:

  1. If the queue is empty (size == 0), return None.
  2. Store the value at front.
  3. Advance front circularly: front = (front + 1) % capacity.
  4. Decrement size and return the stored value.

Full Implementation

class CircularQueue:
def __init__(self, capacity):
self.data = [None] * capacity
self.cap = capacity
self.size = 0
self.front = 0

def enqueue(self, x):
"""Add an element to the rear of the circular queue."""
if self.size == self.cap:
print("Queue is full")
return

# Calculate rear index using modulo for wrap-around
rear = (self.front + self.size) % self.cap
self.data[rear] = x
self.size += 1

def dequeue(self):
"""Remove and return the front element."""
if self.size == 0:
return None

result = self.data[self.front]

# Move front pointer circularly
self.front = (self.front + 1) % self.cap
self.size -= 1
return result

def getFront(self):
"""Return the front element without removing it."""
if self.size == 0:
return None
return self.data[self.front]

def getRear(self):
"""Return the rear element without removing it."""
if self.size == 0:
return None
rear = (self.front + self.size - 1) % self.cap
return self.data[rear]

def isEmpty(self):
"""Check if the queue is empty."""
return self.size == 0

def isFull(self):
"""Check if the queue is full."""
return self.size == self.cap

def getSize(self):
"""Return the current number of elements."""
return self.size

Example Usage and Output

q = CircularQueue(4)

q.enqueue(10)
print(q.getFront(), q.getRear())

q.enqueue(20)
print(q.getFront(), q.getRear())

q.enqueue(30)
print(q.getFront(), q.getRear())

q.enqueue(40)
print(q.getFront(), q.getRear())

q.dequeue()
print(q.getFront(), q.getRear())

q.dequeue()
print(q.getFront(), q.getRear())

# This reuses a slot freed by dequeue: circular wrap-around
q.enqueue(50)
print(q.getFront(), q.getRear())

Output:

10 10
10 20
10 30
10 40
20 40
30 40
30 50

Step-by-step explanation:

  1. 10 is enqueued: front and rear both point to 10.
  2. 20 is enqueued: front is 10, rear is 20.
  3. 30 and 40 are enqueued, filling the queue to capacity.
  4. dequeue() removes 10 and advances front to index 1 (value 20).
  5. dequeue() removes 20 and advances front to index 2 (value 30).
  6. enqueue(50) wraps around to index 0 (the slot previously occupied by 10), demonstrating the circular behavior.

Understanding the Wrap-Around with Modulo

The key to the circular queue is the modulo operator (%). Here's what happens when enqueue(50) is called after two dequeues:

# At this point: front = 2, size = 2, cap = 4
rear = (2 + 2) % 4 # rear = 0: wraps back to the beginning

Without the modulo, the rear index would be 4, which is out of bounds. The modulo ensures it wraps around to 0, reusing the freed slot.

Common Mistake: Not Using Modulo for Wrap-Around

A frequent error when implementing circular queues is forgetting to apply the modulo operation, which causes index-out-of-bounds errors or incorrect behavior:

# ❌ Incorrect: no modulo wrap-around
def enqueue(self, x):
if self.size == self.cap:
return
rear = self.front + self.size # Can exceed array bounds!
self.data[rear] = x # IndexError when rear >= capacity
self.size += 1

def dequeue(self):
if self.size == 0:
return None
result = self.data[self.front]
self.front = self.front + 1 # Front keeps growing, never reuses slots
self.size -= 1
return result
# ✅ Correct: always use modulo
rear = (self.front + self.size) % self.cap
self.front = (self.front + 1) % self.cap
tip

Always apply % capacity when calculating front or rear indices. This is the single operation that makes a circular queue circular.

Comparison: Regular List vs. Circular List

AspectRegular List QueueCircular List Queue
EnqueueO(1)O(1)
DequeueO(n) (element shifting)O(1) (pointer advance)
getFrontO(1)O(1)
getRearO(1)O(1)
Space reuse❌ No wasted-slot recovery✅ Freed slots are reused
Implementation complexitySimpleSlightly more complex

When to Use Python's Built-In Alternatives

Both implementations above are excellent for learning, interviews, and understanding how queues work internally. However, for production code, Python offers built-in options that are already optimized:

from collections import deque

queue = deque()
queue.append(10) # Enqueue
queue.append(20)
print(queue.popleft()) # Dequeue: 10, O(1)
When to use each approach
  • Regular list queue: Only for learning purposes. The O(n) dequeue makes it impractical for real applications.
  • Circular list queue: Great for fixed-size buffer scenarios (for example, streaming data, task schedulers) and coding interviews.
  • collections.deque: The best choice for production Python code; it provides O(1) operations on both ends, is dynamically sized, and is thoroughly optimized.

Conclusion

Implementing a queue using a list in Python can be done in two ways: a regular list approach that is simple but suffers from O(n) dequeue performance, and a circular list approach that achieves O(1) time complexity for all core operations by reusing array slots through modulo arithmetic.

The circular queue is the clear winner when you need a fixed-size, array-based queue.

For general-purpose work in Python, collections.deque remains the recommended tool, but understanding these implementations is essential for mastering data structures and acing technical interviews.