Heaps with heapq

Reviewed & published by Brayan K

A heap is a tree-based structure that always keeps the smallest element ready at the front, and Python's heapq module turns an ordinary list into a binary min-heap so you can push and pop the minimum in fast O(log n) time.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Heaps power priority queues, scheduling, and "top-k" queries. Once you see how heapq works, a whole class of problems becomes easy.

What You'll Learn in This Lesson

1 heappush, heappop & heapify

A heap lives in a plain list. heappush adds an item keeping the heap valid, heappop always removes the smallest, and heapify rearranges an existing list into a heap in place:

import heapq

heap = []
heapq.heappush(heap, 5)
heapq.heappush(heap, 1)
heapq.heappush(heap, 8)
heapq.heappush(heap, 3)

# The smallest is always at index 0
print(heap[0])              # 1

# heappop removes and returns the smallest each time
print(heapq.heappop(heap))  # 1
print(heapq.heappop(heap))  # 3
print(heapq.heappop(heap))  # 5

# heapify turns any list into a heap in O(n), in place
nums = [9, 4, 7, 1, 2]
heapq.heapify(nums)
print(nums[0])              # 1  (smallest now at the front)
import heapq

# Heap-sort: push everything, pop everything
data = [9, 3, 5, 1, 8, 2]
heap = []
for x in data:
    heapq.heappush(heap, x)

ordered = [heapq.heappop(heap) for _ in range(len(heap))]
print(ordered)

# ✅ Expected output:
# [1, 2, 3, 5, 8, 9]

2 Top-k with nlargest & nsmallest

Need just the biggest or smallest few from a large collection? heapq.nlargest(k, data) and nsmallest(k, data) do it in O(n log k) — faster than a full sort when k is small. Both accept a key function just like sorted():

import heapq

scores = [88, 42, 71, 99, 63, 55, 100, 30]

# The 3 highest and 3 lowest
print(heapq.nlargest(3, scores))    # [100, 99, 88]
print(heapq.nsmallest(3, scores))   # [30, 42, 55]

# With a key, just like sorted()
people = [("Ada", 88), ("Ben", 42), ("Cleo", 99)]
top_two = heapq.nlargest(2, people, key=lambda p: p[1])
print(top_two)   # [('Cleo', 99), ('Ada', 88)]

For a million records where you only need the top 10, nlargest(10, data) keeps a heap of just 10 items as it scans once — far cheaper than sorting all million.

3 Priority Queues with Tuples

Push (priority, item) tuples and the heap orders by priority automatically, because Python compares tuples left to right. Add a tie-breaking counter so the heap never compares the items themselves:

import heapq
from itertools import count

pq = []
counter = count()   # 0, 1, 2, ... unique tie-breaker

# (priority, tie_breaker, task) — lower priority number = more urgent
heapq.heappush(pq, (2, next(counter), "email newsletter"))
heapq.heappush(pq, (1, next(counter), "fix production bug"))
heapq.heappush(pq, (3, next(counter), "tidy backlog"))
heapq.heappush(pq, (1, next(counter), "answer on-call page"))

while pq:
    priority, _, task = heapq.heappop(pq)
    print(f"[P{priority}] {task}")

# [P1] fix production bug
# [P1] answer on-call page
# [P2] email newsletter
# [P3] tidy backlog
import heapq

# Max-heap trick: negate values on the way in and out
nums = [3, 1, 4, 1, 5, 9, 2, 6]

max_heap = []
for n in nums:
    heapq.heappush(max_heap, -n)   # store negatives

# Pop the 3 LARGEST by un-negating
largest = [-heapq.heappop(max_heap) for _ in range(3)]
print(largest)

# ✅ Expected output:
# [9, 6, 5]

🎯 Your Turn: Fill in the Blanks

Replace each ___ so the code pushes tasks by priority and pops the most urgent first.

# 🎯 YOUR TURN: pop tasks in priority order
import heapq

pq = []
heapq.___(pq, (3, "low"))      # push onto the heap
heapq.heappush(pq, (1, "high"))
heapq.heappush(pq, (2, "medium"))

order = []
while pq:
    priority, name = heapq.___(pq)   # pop the smallest priority
    order.append(name)

print(order)

# Expected output:
# ['high', 'medium', 'low']

Common Errors

❌ Expecting the heap list to be sorted

heap = [5, 1, 8]
heapq.heapify(heap)
print(heap)   # [1, 5, 8] here, but NOT guaranteed fully sorted in general

✅ Pop repeatedly for sorted output

ordered = [heapq.heappop(heap) for _ in range(len(heap))]

❌ Tuples whose items can't be compared on a tie

heapq.heappush(pq, (1, {"a": 1}))
heapq.heappush(pq, (1, {"b": 2}))   # TypeError when priorities tie

✅ Add a unique tie-breaker

heapq.heappush(pq, (1, 0, {"a": 1}))
heapq.heappush(pq, (1, 1, {"b": 2}))   # safe

❌ Trying to pop the largest from a min-heap

heapq.heappop(heap)   # gives the SMALLEST, not the largest

✅ Negate values or use nlargest

biggest = heapq.nlargest(1, data)[0]

🎯 Mini Challenge: Merge Sorted Streams

heapq even has merge(), which lazily merges already-sorted inputs into one sorted stream — exactly how external merge sort works on huge files.

# 🎯 Mini Challenge: merge three sorted lists into one
import heapq

a = [1, 4, 7]
b = [2, 5, 8]
c = [3, 6, 9]

merged = list(heapq.merge(a, b, c))
print(merged)

# Also grab the 3 smallest overall in one call
print(heapq.nsmallest(3, merged))

# Expected output:
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# [1, 2, 3]

📚 Keep Learning

Frequently Asked Questions

Lesson complete — priority queues hold no fear!

You can push and pop the minimum with heappush/heappop, build a heap with heapify, grab the top-k with nlargest/nsmallest, build priority queues from tuples, and flip to a max-heap with the negation trick.

Practice quiz

Is Python's heapq a min-heap or a max-heap?

  • A max-heap — the largest pops first
  • Neither; it is a balanced tree
  • A min-heap — the smallest pops first
  • It depends on insertion order

Answer: A min-heap — the smallest pops first. heapq is always a min-heap: the smallest value sits at index 0 and pops out first.

After heappush'ing 5, 1, 8, 3 onto an empty heap, what is heap[0]?

  • 1
  • 5
  • 8
  • 3

Answer: 1. The smallest pushed value, 1, is always kept at index 0 of the heap.

What does heapq.heappop(heap) return?

  • The largest element
  • The last element pushed
  • A random element
  • The smallest element

Answer: The smallest element. heappop removes and returns the smallest element each time, in O(log n).

What does heapq.nlargest(3, [88, 42, 71, 99, 63, 55, 100, 30]) return?

  • [30, 42, 55]
  • [100, 99, 88]
  • [88, 99, 100]
  • [100, 88, 71]

Answer: [100, 99, 88]. nlargest returns the top k values in descending order: 100, 99, 88.

What does heapq.nsmallest(3, [88, 42, 71, 99, 63, 55, 100, 30]) return?

  • [30, 42, 55]
  • [100, 99, 88]
  • [30, 42, 71]
  • [42, 30, 55]

Answer: [30, 42, 55]. nsmallest returns the bottom k values in ascending order: 30, 42, 55.

What does heapify do to a list?

  • Fully sorts it in place
  • Returns a new sorted list
  • Rearranges it into a valid heap in place in O(n)
  • Removes duplicates

Answer: Rearranges it into a valid heap in place in O(n). heapify reorders the list in place into a heap in O(n); only the smallest is guaranteed at index 0.

Is a heap list (after heapify) guaranteed to be fully sorted?

  • Yes, always
  • No, only the smallest is guaranteed at index 0
  • Yes, in descending order
  • Only if all values are unique

Answer: No, only the smallest is guaranteed at index 0. A heap only guarantees the smallest at index 0; to get sorted output, heappop repeatedly.

How do you make a max-heap behavior with heapq?

  • Use heapq.maxheap()
  • Sort the list first
  • Use heappop twice
  • Push the negated values and negate again when popping

Answer: Push the negated values and negate again when popping. There is no built-in max-heap; push -x and negate on the way out, or use nlargest.

When building a priority queue, why add a tie-breaking counter to (priority, item) tuples?

  • To make popping faster
  • So the heap never has to compare the items themselves when priorities tie
  • To sort the items alphabetically
  • To save memory

Answer: So the heap never has to compare the items themselves when priorities tie. A unique counter breaks ties so Python never compares the items, which could raise a TypeError.

When is nlargest(k, data) preferable to fully sorting the data?

  • When you need every element ordered
  • When the data is already sorted
  • When you only need the top few from a large collection (O(n log k))
  • When k equals the length of the data

Answer: When you only need the top few from a large collection (O(n log k)). nlargest keeps a heap of size k and runs in O(n log k), cheaper than a full O(n log n) sort when k is small.

Continue this course

Frequently asked questions

What is a heap and what is heapq?

A heap is a tree-shaped data structure that always keeps the smallest item instantly accessible at the front. Python's heapq module implements a binary min-heap directly on top of an ordinary list, so heapq.heappop() always removes and returns the smallest element in O(log n) time. It's the engine behind priority queues.

Is Python's heapq a min-heap or a max-heap?

It's always a min-heap: the smallest value sits at index 0 and pops out first. To get max-heap behaviour, push the negated values (-x) and negate again when you pop, or use heapq.nlargest(). There is no built-in max-heap, so the negation trick is the standard workaround.

How do I build a priority queue with heapq?

Push tuples of (priority, item) onto the heap. Python compares tuples element by element, so the lowest priority number pops first. If two priorities tie, add a unique counter as a second element — like (priority, count, item) — so the heap never has to compare the items themselves, which could raise a TypeError.

When should I use nlargest/nsmallest instead of sorting?

When you only need the top or bottom few from a large collection. heapq.nlargest(k, data) runs in O(n log k) and uses a heap of size k, whereas fully sorting the data is O(n log n). For k much smaller than n — like the top 10 from a million records — the heap approach is meaningfully faster and lighter on memory.