Skip to content

How to Implement a Queue Using a List in Python

Learn how to implement a queue in Python using both a regular list (with O(n) dequeue) and a circular list (with O(1) dequeue), comparing performance trade-offs and when to use each approach.

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

        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:

for i in range(1, self.size):
    self.arr[i - 1] = self.arr[i]
Operation Time Complexity
Enqueue O(1)
Dequeue O(n), due to element shifting
getFront O(1)
Display O(n)

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

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

        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]

        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())

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:

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:

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
rear = (self.front + self.size) % self.cap
self.front = (self.front + 1) % self.cap

Comparison: Regular List vs. Circular List

Aspect Regular List Queue Circular List Queue
Enqueue O(1) O(1)
Dequeue O(n) (element shifting) O(1) (pointer advance)
getFront O(1) O(1)
getRear O(1) O(1)
Space reuse ❌ No wasted-slot recovery ✅ Freed slots are reused
Implementation complexity Simple Slightly 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)

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.

Navigation