I hit this problem every time I move from toy examples to real workloads: I want a heap because I need fast access to the smallest or largest item, but my data is not a list of simple numbers. It is usually a list of dictionaries, domain objects, or mixed records pulled from a database. The heapq module looks simple, yet it does not accept a custom comparator the way some other languages do. If you try to heapify a dictionary or a list of dicts, Python raises a TypeError because it cannot compare those objects directly. That makes it feel like heaps are only for integers. In practice, you can still build heaps over any data structure if you reshape the data first or teach your objects how to compare themselves. In this post I show the exact patterns I use in production: tuple decoration, wrapper objects, and a few modern 2026-friendly helpers that keep code readable. You will leave with ready-to-run examples, guidance on edge cases, and a clear sense of when a heap is the right tool and when it is the wrong one.
Why heapq feels limiting at first
The heapq module implements a binary min-heap using a plain Python list. The only rule is: items must be comparable with the < operator. When the items are numbers or strings, that is trivial. When the items are dictionaries, class instances, or nested structures, Python does not know what to compare, so it refuses. I see three early surprises that catch people:
1) heapq.heapify() requires a list, not any iterable. A dictionary is iterable, but it iterates keys, which is not what you want.
2) A list of dictionaries still fails, because dictionaries do not support <.
3) Even for objects that look comparable, ties can break the heap if the fallback comparison fails.
In other words, the heap does not care about your data, only about a deterministic < comparison. That means you must either convert your data into a comparable form or define how to compare your objects. I like to think about this as giving the heap a scorecard it understands. Once the scorecard exists, all the heap operations work as expected.
The mental model: decorate, heapify, undecorate
When I teach this, I use a simple analogy: imagine a school science fair. Every project is complicated, but the judges only look at the score on the ribbon. The heap is the judge. If you attach a score to each project, the judge can sort quickly without understanding the project itself.
That is the decorate-heapify-undecorate model:
- Decorate your item with a comparable key (often a tuple).
- Use
heapqon the decorated list. - Undecorate when you pop or peek.
This pattern is the core of custom ordering. It works for dictionaries, objects, and mixed data. A tuple is the usual decorator because Python compares tuples lexicographically: first element, then second, and so on. That lets you add tie-breakers to keep the heap stable.
Custom ordering with tuples and key functions
If your data is a dictionary or a list of dictionaries, the simplest approach is to convert to a list of tuples. That gives the heap a clear ordering. The following example builds a min-heap by dictionary key:
import heapq as hq
my_dict = {‘z‘: ‘zebra‘, ‘b‘: ‘ball‘, ‘w‘: ‘whale‘, ‘a‘: ‘apple‘, ‘m‘: ‘monkey‘, ‘c‘: ‘cat‘}
Turn key-value pairs into a list of tuples
heap = [(k, v) for k, v in my_dict.items()]
hq.heapify(heap)
Peek at the smallest key
print(heap[0]) # (‘a‘, ‘apple‘)
Pop in sorted-by-key order
while heap:
print(hq.heappop(heap))
That approach is enough if ordering by key is acceptable. But you can also order by value or by computed score. Here I use the key length as the sort key and keep a counter to avoid comparison errors on ties:
import heapq as hq
from itertools import count
my_dict = {‘z‘: ‘zebra‘, ‘b‘: ‘ball‘, ‘w‘: ‘whale‘, ‘a‘: ‘apple‘, ‘m‘: ‘monkey‘, ‘c‘: ‘cat‘}
counter = count()
heap = [(len(v), next(counter), k, v) for k, v in my_dict.items()]
hq.heapify(heap)
while heap:
length, _, k, v = hq.heappop(heap)
print(length, k, v)
Notice the counter value in the tuple. When two items share the same length, the heap tries the next element of the tuple. If that element is an object that cannot be compared (like a dict or custom class), you will get a TypeError. The counter guarantees a strict ordering and prevents that crash. I treat this as the safest pattern for all production heaps.
A custom predicate, Python style
Python does not allow a custom comparator function for heapq. The nearest equivalent is to convert your custom predicate to a key function, then decorate items with that key. If your predicate says, for example, "compare by negative priority, then by creation timestamp", you can translate that into a tuple like (-priority, created_at, item).
Here is a runnable example for a list of dictionaries representing build jobs. The heap returns highest priority first and uses an ISO timestamp as a tie-breaker:
import heapq as hq
from itertools import count
jobs = [
{‘id‘: ‘build-141‘, ‘priority‘: 5, ‘created_at‘: ‘2026-01-20T10:30:00Z‘},
{‘id‘: ‘build-142‘, ‘priority‘: 10, ‘created_at‘: ‘2026-01-20T10:29:00Z‘},
{‘id‘: ‘build-143‘, ‘priority‘: 10, ‘created_at‘: ‘2026-01-20T10:31:00Z‘},
]
counter = count()
heap = [(-job[‘priority‘], job[‘created_at‘], next(counter), job) for job in jobs]
hq.heapify(heap)
while heap:
, , _, job = hq.heappop(heap)
print(job[‘id‘])
That is the "custom predicate" in practice: it is just a key that the heap can compare. I use this for job schedulers, request throttling, and rate-limit queues.
A traditional vs modern comparison
When I help teams modernize legacy code, I often see hand-rolled priority queues based on repeated sorting. Heaps are faster for frequent push/pop cycles. This table shows the tradeoffs I use when advising teams in 2026:
Insert cost
Typical use
—
—
O(n log n)
Small lists
heapq with tuple keys O(log n)
Task queues
nlargest/nsmallest with key O(n log k)
One-off top-k
If you are doing many pushes and pops, I recommend the heap approach. If you only need a top-k list once, heapq.nlargest is fine because it accepts a key argument directly.
Wrapper classes and rich comparison
Sometimes you do not want to keep tuples around. That is common if your data is a rich object and you want a clean heap list with only objects inside it. In that case, I create a wrapper class that defines lt. The heap will use that comparison and ignore the rest.
Here is a complete example with employees ordered by years of service. Notice the tie-breaker on name to keep the ordering stable:
import heapq as hq
class Employee:
def init(self, name, designation, yearsofservice, salary):
self.name = name
self.designation = designation
self.yearsofservice = yearsofservice
self.salary = salary
def lt(self, other):
# Primary sort: years of service
if self.yearsofservice != other.yearsofservice:
return self.yearsofservice < other.yearsofservice
# Tie-breaker: name to avoid unstable ordering
return self.name < other.name
def repr(self):
return f‘Employee({self.name}, {self.designation}, {self.yearsofservice}, {self.salary})‘
staff = [
Employee(‘Amina‘, ‘Engineer‘, 3, 120000),
Employee(‘Jae‘, ‘Engineer‘, 5, 140000),
Employee(‘Ravi‘, ‘Manager‘, 5, 165000),
Employee(‘Lena‘, ‘Engineer‘, 1, 90000),
]
hq.heapify(staff)
while staff:
print(hq.heappop(staff))
I prefer this approach when the heap itself is long-lived and the objects are reused in multiple places. The tradeoff is that you are now making a global comparison rule for that class, which might not match every situation. If you need different ordering rules in different contexts, tuple decoration is safer.
Wrapper objects to avoid global ordering
If you need per-heap comparison logic, I wrap the item instead of editing the class:
import heapq as hq
class HeapItem:
def init(self, key, item):
self.key = key
self.item = item
def lt(self, other):
return self.key < other.key
jobs = [
{‘id‘: ‘build-201‘, ‘priority‘: 3, ‘age_minutes‘: 5},
{‘id‘: ‘build-202‘, ‘priority‘: 2, ‘age_minutes‘: 30},
{‘id‘: ‘build-203‘, ‘priority‘: 3, ‘age_minutes‘: 2},
]
heap = [HeapItem((-j[‘priority‘], -j[‘age_minutes‘]), j) for j in jobs]
hq.heapify(heap)
while heap:
print(hq.heappop(heap).item)
This keeps your domain objects clean and avoids the risk of unintended ordering in other parts of your codebase.
Modern patterns: dataclasses, typing, and AI-assisted checks (2026)
Python now has mature support for dataclasses and strong typing. I use dataclasses with order=True when the comparison is a natural part of the object. You still need to make sure non-order fields are excluded from comparison. Here is the pattern I rely on:
from dataclasses import dataclass, field
import heapq as hq
@dataclass(order=True)
class Ticket:
priority: int
created_at: str
id: str = field(compare=False)
customer: str = field(compare=False)
heap = [
Ticket(1, ‘2026-01-20T10:40:00Z‘, ‘T-100‘, ‘Liam‘),
Ticket(2, ‘2026-01-20T10:35:00Z‘, ‘T-101‘, ‘Maya‘),
Ticket(1, ‘2026-01-20T10:50:00Z‘, ‘T-102‘, ‘Noah‘),
]
hq.heapify(heap)
while heap:
print(hq.heappop(heap))
That gives a clean ordering without extra code. I recommend this when your data model already uses dataclasses and when the ordering makes sense for the model itself.
In 2026, I also see teams use AI-assisted checks to avoid subtle errors in the comparison logic. A common bug is forgetting a tie-breaker and then getting a TypeError on equal keys. I keep a small set of tests and use an AI linting step to check for comparison methods that do not handle equality. You do not need a special tool for this; even a basic unit test that pushes two equal-priority items into a heap is enough to catch it.
Traditional vs modern method table
Here is the comparison I share with teams when choosing the implementation style:
Readability
Risk of tie errors
—
—
High
Low if you add a counter
Medium
Low if key is stable
order=True High
Medium if fields are misconfigured
If you want one rule everywhere, dataclasses are clean. If you need custom order per heap, tuple decoration or a wrapper is safer.
Performance, mistakes, and when not to use a heap
A heap is a great fit when you need frequent insert and remove-min operations. It is not always the best choice, so I use these guidelines:
When to use a heap
- You need many pushes and pops over time.
- You care about the smallest or largest element at each step.
- You can compute a stable key that defines priority.
When not to use a heap
- You just need a sorted list once. A single
sorted()call is simpler. - You need random access or removal of arbitrary elements. Heaps do not handle that cleanly.
- Your comparison key changes often after insertion. Heaps do not support key updates in place.
Common mistakes I see
1) Forgetting a tie-breaker, which leads to TypeError if keys are equal and the next tuple element is unorderable.
2) Using mutable objects inside the key, which can change after heap insertion and break ordering.
3) Mixing types in the key, like int and str, which Python cannot compare.
4) Assuming heapq supports a custom comparator. It does not. You must build a key.
Performance considerations
Heap operations are O(log n). In practice, I see push/pop times in the 10–50 microsecond range for heaps with thousands of elements, and often in the 1–5 millisecond range when you are doing heavy object creation or very large heaps. The actual range depends on object size and Python version, but the growth is predictable: doubling heap size does not double the cost. That is why heaps are great for long-running queues.
Practical recipes and edge cases
This section is a grab bag of patterns I use often.
Recipe 1: Keep a max-heap for top scores
heapq is a min-heap. To simulate a max-heap, negate the key:
import heapq as hq
scores = [
{‘name‘: ‘Elena‘, ‘score‘: 92},
{‘name‘: ‘Kai‘, ‘score‘: 88},
{‘name‘: ‘Priya‘, ‘score‘: 99},
]
heap = [(-s[‘score‘], s[‘name‘], s) for s in scores]
hq.heapify(heap)
while heap:
, , s = hq.heappop(heap)
print(s)
Recipe 2: Stable ordering with a monotonic counter
If your key can be equal, always add a counter:
import heapq as hq
from itertools import count
counter = count()
events = [
{‘type‘: ‘deploy‘, ‘priority‘: 1},
{‘type‘: ‘alert‘, ‘priority‘: 1},
{‘type‘: ‘backup‘, ‘priority‘: 2},
]
heap = [(e[‘priority‘], next(counter), e) for e in events]
hq.heapify(heap)
while heap:
print(hq.heappop(heap)[2])
Recipe 3: Heap from a list of dicts with computed keys
Here is a complete example that orders by a computed score. I treat higher severity and older timestamps as more urgent:
import heapq as hq
from itertools import count
from datetime import datetime
alerts = [
{‘id‘: ‘A-1‘, ‘severity‘: 5, ‘created_at‘: ‘2026-01-20T10:00:00Z‘},
{‘id‘: ‘A-2‘, ‘severity‘: 3, ‘created_at‘: ‘2026-01-20T09:50:00Z‘},
{‘id‘: ‘A-3‘, ‘severity‘: 5, ‘created_at‘: ‘2026-01-20T09:55:00Z‘},
]
counter = count()
Negative severity for max behavior, older timestamps first
heap = []
for a in alerts:
created = datetime.fromisoformat(a[‘created_at‘].replace(‘Z‘, ‘+00:00‘))
heap.append((-a[‘severity‘], created, next(counter), a))
hq.heapify(heap)
while heap:
print(hq.heappop(heap)[3][‘id‘])
Recipe 4: Use nsmallest with a key for one-off queries
If you do not need a live heap, use nsmallest or nlargest with a key function:
import heapq as hq
orders = [
{‘id‘: ‘O-1‘, ‘total‘: 120.50},
{‘id‘: ‘O-2‘, ‘total‘: 35.25},
{‘id‘: ‘O-3‘, ‘total‘: 78.00},
]
smallest = hq.nsmallest(2, orders, key=lambda o: o[‘total‘])
print(smallest)
This is not a heap you can push to, but it is simple and readable for top-k selection.
Edge case: mixed numeric and string keys
Do not allow mixed types in the key. Python 3 will not compare int and str. Normalize keys before you build the heap:
import heapq as hq
records = [
{‘id‘: ‘1‘, ‘priority‘: ‘3‘},
{‘id‘: ‘2‘, ‘priority‘: 2},
]
Normalize: coerce to int and add a counter for stability
heap = []
for i, r in enumerate(records):
priority = int(r[‘priority‘])
heap.append((priority, i, r))
hq.heapify(heap)
while heap:
print(hq.heappop(heap)[2])
Edge case: missing keys in dictionaries
In production data, keys can be missing. A missing key should not crash the heap build. I usually normalize with defaults or fallbacks:
import heapq as hq
from itertools import count
rows = [
{‘id‘: ‘X-1‘, ‘priority‘: 3},
{‘id‘: ‘X-2‘},
{‘id‘: ‘X-3‘, ‘priority‘: 1},
]
counter = count()
heap = []
for r in rows:
priority = r.get(‘priority‘, 9999) # default low priority
heap.append((priority, next(counter), r))
hq.heapify(heap)
while heap:
print(hq.heappop(heap)[2])
The default should reflect your business rule. In this case missing priority goes last.
Edge case: comparing timestamps correctly
String timestamps can compare lexicographically if they are consistent ISO format. But if you mix formats, you can get wrong ordering. I always parse to datetime when there is any risk of inconsistent formats:
import heapq as hq
from datetime import datetime
from itertools import count
records = [
{‘id‘: ‘S-1‘, ‘created_at‘: ‘2026-01-20T10:05:00Z‘},
{‘id‘: ‘S-2‘, ‘created_at‘: ‘2026/01/20 10:04:00‘},
]
counter = count()
heap = []
for r in records:
createdat = r[‘createdat‘]
if ‘T‘ in created_at:
dt = datetime.fromisoformat(created_at.replace(‘Z‘, ‘+00:00‘))
else:
dt = datetime.strptime(created_at, ‘%Y/%m/%d %H:%M:%S‘)
heap.append((dt, next(counter), r))
hq.heapify(heap)
while heap:
print(hq.heappop(heap)[2])
Edge case: priority inversion and fairness
If you always use strict priority, low-priority items can starve forever. The heap itself does not solve fairness; you have to encode it in the key. A simple technique is to combine priority and wait time so waiting items slowly rise:
import heapq as hq
from datetime import datetime, timezone
from itertools import count
jobs = [
{‘id‘: ‘J-1‘, ‘priority‘: 10, ‘created_at‘: ‘2026-01-20T10:00:00Z‘},
{‘id‘: ‘J-2‘, ‘priority‘: 1, ‘created_at‘: ‘2026-01-20T09:00:00Z‘},
]
counter = count()
now = datetime(2026, 1, 20, 11, 0, 0, tzinfo=timezone.utc)
heap = []
for j in jobs:
created = datetime.fromisoformat(j[‘created_at‘].replace(‘Z‘, ‘+00:00‘))
waitminutes = (now - created).totalseconds() / 60
# Higher priority and longer wait -> smaller key
key = (-(j[‘priority‘] + wait_minutes / 60), next(counter))
heap.append((key, j))
hq.heapify(heap)
while heap:
print(hq.heappop(heap)[1])
This is a simple fairness trick for queues under sustained load.
Translating a comparator into a heap key
Developers often come from languages where you can pass a comparator. The translation to a Python heap is mostly mechanical. I use this checklist:
1) Identify the primary comparison.
2) Identify tie-breakers in order.
3) Convert each comparison to a sortable key field.
4) For descending order, negate numbers or invert sorting with - or custom mapping.
5) Append a counter to guarantee a strict ordering.
Here is a side-by-side conversion. Imagine the following comparator in another language:
- Higher priority first
- Older
created_atfirst - If still tied, by
id
In Python with heapq you would build:
from itertools import count
counter = count()
heap = []
for item in items:
key = (-item[‘priority‘], item[‘created_at‘], item[‘id‘], next(counter))
heap.append((key, item))
The idea is not just to “make it work” but to make it obvious for future you why the order is correct.
A deeper production example: in-memory task scheduler
Here is a complete pattern for a small in-memory scheduler. It supports:
- dynamic inserts
- peek without pop
- max-priority behavior
- stable tie-breaking
import heapq as hq
from itertools import count
from datetime import datetime, timezone
class TaskQueue:
def init(self):
self._heap = []
self._counter = count()
def push(self, task):
# task = {‘id‘: str, ‘priority‘: int, ‘created_at‘: datetime}
key = (-task[‘priority‘], task[‘createdat‘], next(self.counter))
hq.heappush(self._heap, (key, task))
def pop(self):
if not self._heap:
return None
return hq.heappop(self._heap)[1]
def peek(self):
if not self._heap:
return None
return self._heap[0][1]
def len(self):
return len(self._heap)
queue = TaskQueue()
queue.push({‘id‘: ‘T-1‘, ‘priority‘: 2, ‘created_at‘: datetime(2026, 1, 20, 10, 0, tzinfo=timezone.utc)})
queue.push({‘id‘: ‘T-2‘, ‘priority‘: 5, ‘created_at‘: datetime(2026, 1, 20, 10, 1, tzinfo=timezone.utc)})
queue.push({‘id‘: ‘T-3‘, ‘priority‘: 5, ‘created_at‘: datetime(2026, 1, 20, 9, 59, tzinfo=timezone.utc)})
print(queue.peek())
while queue:
print(queue.pop())
I like this example because it shows a real interface you can drop into a service. The shape mirrors production queue code, but still keeps the heap local and transparent.
Deleting or updating items in a heap
One limitation of heapq is that it does not support removing or updating arbitrary elements. This matters when priorities change. I use two strategies:
Strategy 1: Lazy deletion
You push updated records as new entries and mark the old ones as invalid. When popping, you discard invalid entries until you find a valid one. This keeps the heap operations cheap.
import heapq as hq
from itertools import count
class LazyHeap:
def init(self):
self._heap = []
self._counter = count()
self._valid = set()
def push(self, item, key):
token = (item[‘id‘], next(self._counter))
self._valid.add(token)
hq.heappush(self._heap, (key, token, item))
def invalidate(self, token):
self._valid.discard(token)
def pop(self):
while self._heap:
key, token, item = hq.heappop(self._heap)
if token in self._valid:
self._valid.remove(token)
return item
return None
This is a great fit for queues where updates are frequent but you still want log-time operations.
Strategy 2: Rebuild occasionally
If updates are rare, it can be simpler to rebuild the heap from scratch after a batch of changes. Rebuilding is O(n), so it is often fine if you do it occasionally.
import heapq as hq
items = [{‘id‘: ‘A‘, ‘priority‘: 5}, {‘id‘: ‘B‘, ‘priority‘: 1}]
heap = [(i[‘priority‘], i) for i in items]
hq.heapify(heap)
Later, update priorities
items[0][‘priority‘] = 0
heap = [(i[‘priority‘], i) for i in items]
hq.heapify(heap)
I treat this as a reasonable choice for small heaps or infrequent updates.
Custom predicate patterns in the real world
Here are the actual ordering rules I see in production and how I encode them.
Pattern: SLA tiers + age
Use priority tiers but allow older tasks to rise:
from itertools import count
counter = count()
heap = []
for task in tasks:
key = (-task[‘tier‘], -task[‘age_hours‘], next(counter))
heap.append((key, task))
Pattern: Weighted score
Compute a score from multiple fields:
from itertools import count
counter = count()
heap = []
for item in items:
score = item[‘severity‘] 10 + item[‘impact‘] 2 - item[‘noise‘]
heap.append((-score, next(counter), item))
Pattern: Region affinity
You want items from the current region first, then by priority:
from itertools import count
counter = count()
current_region = ‘us-east‘
heap = []
for job in jobs:
regionbias = 0 if job[‘region‘] == currentregion else 1
heap.append((region_bias, -job[‘priority‘], next(counter), job))
The idea is always the same: the heap doesn’t know your domain, so encode it into a numeric or lexicographic key.
Testing your heap logic
I keep a tiny test checklist for heaps because errors can be subtle:
- Two items with equal priority do not crash when popped
- Ordering is correct for top 3 and bottom 3 items
- Max-heap inversion returns the highest priority first
- Date parsing is consistent
- Mixed types are normalized
Here is a tiny test snippet I use when I introduce a new heap in a project:
import heapq as hq
from itertools import count
def build_heap(records):
counter = count()
heap = [(r[‘priority‘], next(counter), r) for r in records]
hq.heapify(heap)
return heap
records = [
{‘id‘: ‘A‘, ‘priority‘: 1},
{‘id‘: ‘B‘, ‘priority‘: 1},
{‘id‘: ‘C‘, ‘priority‘: 0},
]
heap = build_heap(records)
assert hq.heappop(heap)[2][‘id‘] == ‘C‘
assert hq.heappop(heap)[2][‘id‘] in {‘A‘, ‘B‘}
This is a cheap way to catch the most common ordering bugs.
When a heap is the wrong tool
This is as important as knowing how to use one. I’ve seen heaps forced into workflows where they add complexity without benefits.
Not a heap: you only need one sort
If you only need the order once, a plain sorted() is clearer, and usually fast enough. The overhead of maintaining a heap is wasted.
Not a heap: you need deletions by ID
If you must frequently remove items by ID or update priorities in place, a heap becomes messy. In those cases, a balanced tree structure, an indexed priority queue, or a database query may be more appropriate.
Not a heap: you need stable ordering across multiple keys with heavy reshaping
You can still use a heap, but it might be simpler to precompute a fully sorted list and iterate.
Alternatives to heapq
Sometimes I use these alternatives instead of heapq:
sorted()for a one-time orderingbisectfor maintaining a sorted list when n is smallqueue.PriorityQueueif I want thread-safe producers/consumers- external systems like Redis sorted sets for distributed priority queues
The key lesson: use heaps when you need many push/pop operations in the same process and when you can compute a stable key.
Deeper explanation: why the counter works
People ask why the counter is important. The short answer: tuples are compared element by element. If the first elements are equal, Python compares the next ones. If that next element is unorderable, the heap crashes. A counter ensures all tuples are strictly ordered. Even if you never see a crash during tests, it can happen with real data because equality is more common at scale.
I do not treat the counter as optional; it is a safety belt.
Performance tuning notes for large heaps
When you scale to hundreds of thousands of elements, small choices matter:
- Avoid expensive key computations inside the push loop; precompute if you can
- Reuse objects where possible; heavy object creation can dominate heap time
- If you can store a numeric key instead of a tuple of objects, do it
- Minimize conversions in tight loops (especially timestamp parsing)
If you are doing tens of thousands of pushes per second, the heap operations themselves are rarely the bottleneck; key computation and object creation are. I profile there first.
A practical monitoring example: alert queue
Here’s a realistic alert queue that groups by severity and then by time. It also enforces an “age boost” so old alerts rise even if severity is lower.
import heapq as hq
from itertools import count
from datetime import datetime, timezone
alerts = [
{‘id‘: ‘AL-1‘, ‘severity‘: 5, ‘created_at‘: ‘2026-01-20T10:00:00Z‘},
{‘id‘: ‘AL-2‘, ‘severity‘: 1, ‘created_at‘: ‘2026-01-20T08:30:00Z‘},
{‘id‘: ‘AL-3‘, ‘severity‘: 3, ‘created_at‘: ‘2026-01-20T09:00:00Z‘},
]
counter = count()
now = datetime(2026, 1, 20, 11, 0, 0, tzinfo=timezone.utc)
heap = []
for a in alerts:
created = datetime.fromisoformat(a[‘created_at‘].replace(‘Z‘, ‘+00:00‘))
agehours = (now - created).totalseconds() / 3600
score = a[‘severity‘] * 10 + age_hours
heap.append((-score, next(counter), a))
hq.heapify(heap)
while heap:
print(hq.heappop(heap)[2])
This is not the only way to do it, but it shows how to encode domain logic without a custom comparator.
Key takeaways
heapqdoes not accept a comparator; you must encode a key- Tuple decoration is the most flexible and common pattern
- Always add a tie-breaker like a counter to avoid crashes
- Wrapper classes and dataclasses are clean when ordering is part of the model
- Heaps are ideal for frequent push/pop, not for one-time sorting or random access
If you keep those rules in mind, heaps are a powerful and reliable tool even with complex data structures. The trick is not to fight heapq, but to give it a key it understands.


