Skip to Content
Course content

399: Implementing a Priority Queue with heapq

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're building a background task processor for a web app. Most of your tasks are low-priority, like updating a user's "last seen" timestamp, but occasionally a critical task comes in—like sending a password reset email—that needs to jump to the front of the line. You need a priority queue.

The temptation of the sorted list

When I first encountered this problem, my instinct was to keep a simple Python list of tasks and just sort it whenever I needed to pull the next job. It seems intuitive: you push a tuple of (priority, task_name) into a list, call list.sort(), and then pop(0) the item with the lowest priority number. It works perfectly for ten tasks. It might even work for a hundred.

# The "Naive" Way
tasks = []
tasks.append((3, "Update Analytics"))
tasks.append((1, "Password Reset"))
tasks.append((2, "Send Welcome Email"))

# Every time we want a task, we sort the whole list
tasks.sort() 
next_task = tasks.pop(0) # (1, "Password Reset")

The problem is that sorting is expensive. In Python, sort() uses Timsort, which is incredibly efficient, but it's still an O(N log N) operation. If your task queue grows to thousands of items and you're sorting the entire thing every single time you want to process one item, you're burning CPU cycles for no reason. Even if you used bisect.insort() to keep the list sorted as you added items, you're still dealing with O(N) time complexity for insertions because shifting elements in a list is slow.

Letting the heap do the heavy lifting

This is where heapq comes in. Instead of maintaining a fully sorted list, a heap maintains a "semi-sorted" structure called a binary heap. It doesn't care if the whole list is perfectly in order; it only guarantees that the smallest element (the root) is always at the front. This changes the game: pushing a new item and popping the smallest item both happen in O(log N) time.

import heapq

# We start with a plain list, then "heapify" it or use heappush
task_queue = []

# O(log N) insertion
heapq.heappush(task_queue, (3, "Update Analytics"))
heapq.heappush(task_queue, (1, "Password Reset"))
heapq.heappush(task_queue, (2, "Send Welcome Email"))

# O(log N) removal of the smallest element
priority, task = heapq.heappop(task_queue)
print(f"Processing: {task}") # Processing: Password Reset

I love heapq because it's a thin wrapper around a standard list. You aren't creating a complex object; you're just using a set of functions to manipulate a list in a specific way. It's lean and fast.

The "Max-Heap" gotcha

One thing that always trips people up is that heapq is strictly a min-heap. It always pops the smallest value. But in many business requirements, a "Priority 10" task is more important than a "Priority 1" task. If you try to use heapq with those numbers, your most important tasks will sit at the bottom of the pile until everything else is finished.

The workaround is a simple trick I use every time: invert the priority. If you multiply your priority numbers by -1 before pushing them onto the heap, the largest number becomes the smallest negative number. When you pop it, just multiply it by -1 again to get your original value back. It's a bit of a hack, but it's the standard way to implement a max-priority queue in Python.




📋 Practical Task

Exercise: Building an Emergency Room Triage System

You are tasked with building a triage system for a hospital emergency room. Patients arrive with different urgency levels (1 being the most critical/immediate, 5 being the least urgent). Since multiple patients might arrive with the same urgency level, you must also track their arrival order so that patients with the same priority are treated on a first-come, first-served basis.

Requirements:

  • Create a class TriageQueue.
  • Implement an add_patient(name, priority) method. To handle the "first-come, first-served" requirement for equal priorities, you'll need to keep a global counter (an integer that increments every time a patient is added) and store it as part of the tuple in the heap.
  • Implement a treat_patient() method that pops and returns the name of the patient who should be seen next.
  • Use the heapq module to ensure the operations are efficient.

Test Case:

triage = TriageQueue()
triage.add_patient("Alice", 3)
triage.add_patient("Bob", 1)
triage.add_patient("Charlie", 1)
triage.add_patient("Dave", 2)

print(triage.treat_patient()) # Should be Bob (Priority 1, arrived first)
print(triage.treat_patient()) # Should be Charlie (Priority 1, arrived second)
print(triage.treat_patient()) # Should be Dave (Priority 2)
print(triage.treat_patient()) # Should be Alice (Priority 3)
Rating
0 0

There are no comments for now.

to be the first to leave a comment.