Python's new max-heap is worth it for anything you can't negate
Until 3.14, heapq only did min-heaps. If you wanted the largest item first, you either pushed -x or wrapped each item in a class with a reversed __lt__. Python 3.14 made heappush_max, heappop_max, heapify_max and friends public. I measured them against both old tricks. Against negation the gain is small, 5 to 15%. Against a wrapper class it is 2.5 to 4.5ร, and for strings, which you can't negate, a wrapper was the only option.
The test
CPython 3.14.8. Push N random items one at a time, then pop them all, and check the output comes out sorted largest first. I timed ns per item (one push plus one pop), took the median of 7 to 21 runs, and ran each variant in its own fresh process, three rounds in shuffled order. The machine had other work on it, so call differences under about 15% ties.
| items | N | heappush_max | negate | wrapper class |
|---|---|---|---|---|
| int | 1,000 | 94 | 107 | 408 |
| int | 100,000 | 196 | 218 | 690 |
| int | 1,000,000 | 481 | 511 | 1,742 |
| str | 1,000 | 105 | โ | 398 |
| str | 100,000 | 230 | โ | 715 |
| str | 1,000,000 | 601 | โ | 2,019 |
| (priority, name) | 1,000 | 177 | 213 | 458 |
| (priority, name) | 100,000 | 339 | 479 | 882 |
| (priority, name) | 1,000,000 | 809 | 1,281 | 1,952 |
Floats came out like ints. For the tuples, "negate" means pushing (-p, name) and rebuilding the tuple on the way out. That only works because the priority is a number.
Why it comes out this way
Negating a number is one cheap C-level operation per push and per pop, so a native max-heap only saves that. With tuples it saves more, 20 to 60%, because you build a new tuple on the way in and another on the way out.
The wrapper is slow because every comparison becomes a Python method call. I counted them: a push took about 2.3 comparisons, and a pop took 9 at a thousand items and 19 at a million. That's about 21 calls to __lt__ per item at a million, so the extra 1.3 ยตs per item comes to roughly 60 ns per call. __slots__ didn't help. A plain @total_ordering class was within about 10% of the slotted one, because the cost is the call itself, not looking up the attribute.
One more thing: if all the items are there up front, heapify_max and then popping beat pushing them one at a time by 5 to 30%, because building a heap all at once is linear.
So what
If you are already on 3.14 and you negate numbers, switching is a tidy-up, not a speedup. If you have a ReverseOrder wrapper class for strings, tuples or anything else that can't be negated, delete it: the built-in is three to four times faster on strings and about 2.5ร on tuples, and your code no longer has a class just to flip a comparison. One catch: the _max functions only reverse the comparison. There's no key=, so a heap of records still needs tuples or a class.
Method: one machine, about 100 lines of plain Python, random data from a fixed seed. These are tight-loop costs, not what a whole program would see.