The easiest way is to invert the value of the keys and use heapq. For example, turn 1000.0 into -1000.0 and 5.0 into -5.0.

Answer from Daniel Stutzbach on Stack Overflow
🌐
Python
docs.python.org › 3 › library › heapq.html
heapq — Heap queue algorithm
The root, maxheap[0], contains the largest element; heap.sort(reverse=True) maintains the max-heap invariant. The heapq API differs from textbook heap algorithms in two aspects: (a) We use zero-based indexing. This makes the relationship between the index for a node and the indexes for its children slightly less obvious, but is more suitable since Python uses zero-based indexing.
🌐
GeeksforGeeks
geeksforgeeks.org › python › max-heap-in-python
Max Heap in Python - GeeksforGeeks
July 12, 2025 - By default Min Heap is implemented by this class. But we multiply each value by -1 so that we can use it as MaxHeap. ... # Python3 program to demonstrate heapq (Max Heap) from heapq import heappop, heappush, heapify # Create an empty heap h ...
Discussions

data structures - What do I use for a max-heap implementation in Python? - Stack Overflow
Python includes the heapq module for min-heaps, but I need a max-heap. What should I use for a max-heap implementation in Python? More on stackoverflow.com
🌐 stackoverflow.com
How to use Python's heapq as min-heap AND max-heap?
Just add negative values. And file fetching add - to it. More on reddit.com
🌐 r/leetcode
10
9
April 8, 2024
Make max heap functions public in heapq - Ideas - Discussions on Python.org
The heapq module contains some private max-heap variants of its heap functions: _heapify_max, _heappop_max, _heapreplace_max. This exist to support the higher-level functions like merge(). I’d like the _max variants to be made public (remove the underscore prefix), and documented. More on discuss.python.org
🌐 discuss.python.org
5
June 30, 2022
algorithm - Min/Max Heap implementation in Python - Code Review Stack Exchange
I'm refreshing some of my datastructures. I saw this as the perfect opportunity to get some feedback on my code. I'm interested in: Algorithm wise: Is my implementation correct? (The tests say so... More on codereview.stackexchange.com
🌐 codereview.stackexchange.com
June 22, 2018
🌐
PrepBytes
prepbytes.com › home › heap › implementation of max heap in python
Implementation of Max Heap in Python
May 12, 2023 - Max heap is an efficient data structure used to store elements in order from largest to smallest. In this tutorial, we'll see how to implement max heap using python.
🌐
Wondershare EdrawMax
edrawmax.wondershare.com › home › for it service › what is a max heap python
How to Build Max Heap Data Structure: A Step-by-Step Tutorial
October 22, 2025 - Python handles most of the underlying heap operations implicitly while they have to be explicitly coded out in C++. Insertion and deletion of elements have a complexity of O(Log n) in both cases but the constants differ based on indexing calculations, swapping elements, etc. Python's max heap cannot be accessed directly whereas C++ implementation allows direct access to any element through the use of pointers or iterators.
Find elsewhere
🌐
Codemia
codemia.io › knowledge-hub › path › what_do_i_use_for_a_max-heap_implementation_in_python
What do I use for a max-heap implementation in Python?
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises
🌐
Real Python
realpython.com › python-heapq-module
The Python heapq Module: Using Heaps and Priority Queues – Real Python
July 18, 2022 - The practical result of this is that the number of comparisons in a heap is the base-2 logarithm of the size of the tree. Note: Comparisons sometimes involve calling user-defined code using .__lt__(). Calling user-defined methods in Python is a relatively slow operation compared with other operations done in a heap, so this will usually be the bottleneck.
🌐
Verve AI
vervecopilot.com › interview-questions › what-no-one-tells-you-about-max-heap-python-and-interview-performance
What No One Tells You About Max Heap Python And Interview Performance
Python's heapq module provides ... To simulate a max heap using heapq, you must negate the values before pushing them onto the heap and then negate them back when popping [^3]. For example, to add 5 to a max heap, you'd push ...
🌐
Medium
medium.com › @allan.sioson › max-heapify-build-max-heap-and-heapsort-algorithm-in-python-42c4dec70829
Max-Heapify, Build-Max-Heap, and Heapsort Algorithm | by Allan A. Sioson | Medium
October 17, 2023 - Any given array A can be transformed to a max heap by repeatedly using the Max-Heapify algorithm. Let’s call this algorithm as the Build-Max-Heap algorithm. The implementation uses the Max-Heapify algorithm starting from the last node with at least one child up to the root node. An implementation in python is given below:
🌐
GeeksforGeeks
geeksforgeeks.org › dsa › introduction-to-max-heap-data-structure
Introduction to Max-Heap - GeeksforGeeks
March 23, 2026 - DSA Python · Last Updated : 23 Mar, 2026 · A Max-Heap is a Data Structure with the following properties: It is a Complete Binary Tree. The value of the root node must be the largest among all its descendant nodes, and the same property must ...
🌐
Python.org
discuss.python.org › ideas
Make max heap functions public in heapq - #9 by rhettinger - Ideas - Discussions on Python.org
September 29, 2023 - The heapq module contains some private max-heap variants of its heap functions: _heapify_max, _heappop_max, _heapreplace_max. This exist to support the higher-level functions like merge(). I’d like the _max variants to b…
🌐
CodeSignal
codesignal.com › learn › courses › understanding-and-using-trees-in-python › lessons › unraveling-heaps-theory-operations-and-implementations-in-python
Theory, Operations, and Implementations in Python
In simpler terms, in a Max Heap, each parent node is greater than or equal to its child node(s), and in a Min Heap, each parent node is less than or equal to its child node(s).
🌐
Python documentation
docs.python.org › 3 › whatsnew › 3.14.html
What’s new in Python 3.14
February 23, 2026 - The new incremental garbage collector means that maximum pause times are reduced by an order of magnitude or more for larger heaps.
🌐
Codecademy
codecademy.com › article › max-heap
What is a Max-Heap? Complete Guide with Examples | Codecademy
Learn what a max-heap is, how it works, and how to implement insert, delete, and peek operations with Python code and examples.
🌐
Stack Abuse
stackabuse.com › guide-to-heaps-in-python
Guide to Heaps in Python
April 18, 2024 - This means that the smallest element is always at the root (or the first position in the list). If you need a max heap, you'd have to invert order by multiplying elements by -1 or use a custom comparison function. Python's heapq module provides a suite of functions that allow developers to ...
🌐
Python.org
discuss.python.org › ideas
Make max heap functions public in heapq - Ideas - Discussions on Python.org
June 30, 2022 - The heapq module contains some private max-heap variants of its heap functions: _heapify_max, _heappop_max, _heapreplace_max. This exist to support the higher-level functions like merge(). I’d like the _max variants to b…
Top answer
1 of 1
10

Thanks for sharing your code!

I won't cover all your questions but I will try my best.

(warning, long post incoming)

Is my implementation correct? (The tests say so)

As far as I tried to break it I'd say yes it's correct. But see below for more thorough testing methods.

Can it be sped up?

Spoiler alert: yes

First thing I did was to profile change slightly your test file (I called it test_heap.py) to seed the random list generation. I also changed the random.sample call to be more flexible with the sample_size parameter.

It went from

random_numbers = random.sample(range(100), sample_size)

to

random.seed(7777)
random_numbers = random.sample(range(sample_size * 3), sample_size)

So the population from random.sample is always greater than my sample_size. Maybe there is a better way?

I also set the sample size to be 50000 to have a decent size for the next step.

Next step was profiling the code with python -m cProfile -s cumtime test_heap.py . If you are not familiar with the profiler see the doc. I launch the command a few times to get a grasp of the variations in timing, that gives me a baseline for optimization. The original value was:

  7990978 function calls (6561934 primitive calls) in 3.235 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      5/1    0.000    0.000    3.235    3.235 {built-in method builtins.exec}
        1    0.002    0.002    3.235    3.235 test_heap.py:1(<module>)
        1    0.051    0.051    3.233    3.233 test_heap.py:43(automaticTest)
   100009    0.086    0.000    2.759    0.000 heap.py:15(pop)
1400712/100011    1.688    0.000    2.673    0.000 heap.py:70(__siftdown)
  1400712    0.386    0.000    0.386    0.000 heap.py:104(__get_left_child)
  1400712    0.363    0.000    0.363    0.000 heap.py:110(__get_right_child)
   100008    0.064    0.000    0.341    0.000 heap.py:6(push)
228297/100008    0.180    0.000    0.270    0.000 heap.py:85(__siftup)
  1430126    0.135    0.000    0.135    0.000 heap.py:127(comparer)
  1429684    0.128    0.000    0.128    0.000 heap.py:131(comparer)
   228297    0.064    0.000    0.064    0.000 heap.py:98(__get_parent)
        1    0.026    0.026    0.062    0.062 random.py:286(sample)

Now we have a target to beat and a few information on what takes time. I did not paste the entire list of function calls, it's pretty long but you get the idea.

A lot of time is spent in _siftdown and a lot less on _siftup, and a few functions are called many times so let's see if we can fix that.

(I should have started by _siftdown which was the big fish here but for some reason, I started by _siftup, forgive me)

Speeding up _siftup

Before:

def __siftup(self, index):
    current_value = self.__array[index]
    parent_index, parent_value = self.__get_parent(index)
    if index > 0 and self.comparer(current_value, parent_value):
        self.__array[parent_index], self.__array[index] =\
            current_value, parent_value
        self.__siftup(parent_index)
    return

After:

def __siftup(self, index):
    current_value = self.__array[index]
    parent_index = (index - 1) >> 1
    if index > 0:
        parent_value = self.__array[parent_index]
        if self.comparer(current_value, parent_value):
            self.__array[parent_index], self.__array[index] =\
                current_value, parent_value
            self.__siftup(parent_index)
    return

I changed the way to calculate parent_index because I looked at the heapq module source and they use it. (see here) but I couldn't see the difference in timing from this change alone.

Then I removed the call to _get_parent and made the appropriate change (kind of inlining it because function call are not cheap in Python) and the new time is

7762306 function calls (6333638 primitive calls) in 3.147 seconds

Function calls went down obviously but time only dropped around 70-80 millisecond. Not a great victory (a bit less than a 3% speedup). And readability was not improved so up to you if it is worth it.

Speeding up _siftdown

The first change was to improve readability.

Original version:

def __siftdown(self, index):
    current_value = self.__array[index]
    left_child_index, left_child_value = self.__get_left_child(index)
    right_child_index, right_child_value = self.__get_right_child(index)
    # the following works because if the right_child_index is not None, then the left_child
    # is also not None => property of a complete binary tree, else left will be returned.
    best_child_index, best_child_value = (right_child_index, right_child_value) if right_child_index\
    is not None and self.comparer(right_child_value, left_child_value) else (left_child_index, left_child_value)
    if best_child_index is not None and self.comparer(best_child_value, current_value):
        self.__array[index], self.__array[best_child_index] =\
            best_child_value, current_value
        self.__siftdown(best_child_index)
    return

V2:

def __siftdown(self, index): #v2
    current_value = self.__array[index]
    left_child_index, left_child_value = self.__get_left_child(index)
    right_child_index, right_child_value = self.__get_right_child(index)
    # the following works because if the right_child_index is not None, then the left_child
    # is also not None => property of a complete binary tree, else left will be returned.
    best_child_index, best_child_value = (left_child_index, left_child_value)
    if right_child_index is not None and self.comparer(right_child_value, left_child_value):
        best_child_index, best_child_value = (right_child_index, right_child_value)
    if best_child_index is not None and self.comparer(best_child_value, current_value):
        self.__array[index], self.__array[best_child_index] =\
            best_child_value, current_value
        self.__siftdown(best_child_index)
    return

I transformed the ternary assignment

best_child_index, best_child_value = (right_child_index, right_child_value) if right_child_index\
        is not None and self.comparer(right_child_value, left_child_value) else (left_child_index, left_child_value)

into

best_child_index, best_child_value = (left_child_index, left_child_value)
if right_child_index is not None and self.comparer(right_child_value, left_child_value):
    best_child_index, best_child_value = (right_child_index, right_child_value)

I find it a lot more readable but it's probably a matter of taste. And to my surprise, when I profiled the code again, the result was:

7762306 function calls (6333638 primitive calls) in 3.079 seconds

(I ran it 10times and I always had gained around 80-100 milliseconds). I don't really understand why, if anybody could explain to me?

V3:

def __siftdown(self, index): #v3
    current_value = self.__array[index]
    
    left_child_index = 2 * index + 1
    if left_child_index > self.__last_index:
        left_child_index, left_child_value = None, None
    else:
        left_child_value = self.__array[left_child_index]
    
    right_child_index = 2 * index + 2
    if right_child_index > self.__last_index:
         right_child_index, right_child_value = None, None
    else:
        right_child_value = self.__array[right_child_index]
    # the following works because if the right_child_index is not None, then the left_child
    # is also not None => property of a complete binary tree, else left will be returned.
    best_child_index, best_child_value = (left_child_index, left_child_value)
    if right_child_index is not None and self.comparer(right_child_value, left_child_value):
        best_child_index, best_child_value = (right_child_index, right_child_value)
    if best_child_index is not None and self.comparer(best_child_value, current_value):
        self.__array[index], self.__array[best_child_index] =\
            best_child_value, current_value
        self.__siftdown(best_child_index)
    return

Like in _siftup I inlined 2 calls from helper function _get_left_child and _get_right_child and that payed off!

4960546 function calls (3531878 primitive calls) in 2.206 seconds

That's a 30% speedup from the baseline.

(What follow is a further optimization that I try to explain but I lost the code I wrote for it, I'll try to right down again later. It might gives you an idea of the gain)

Then using the heapq trick of specializing comparison for max and min (using a _siftdown_max and _siftup_max version replacing comparer by > and doing the same for min) gives us to:

2243576 function calls (809253 primitive calls) in 1.780 seconds

I did not get further in optimizations but the _siftdown is still a big fish so maybe there is room for more optimizations? And pop and push maybe could be reworked a bit but I don't know how.

Comparing my code to the one in the heapq module, it seems that they do not provide a heapq class, but just provide a set of operations that work on lists? Is this better?

I'd like to know as well!

Many implementations I saw iterate over the elements using a while loop in the siftdown method to see if it reaches the end. I instead call siftdown again on the chosen child. Is this approach better or worse?

Seeing as function call are expensive, looping instead of recursing might be faster. But I find it better expressed as a recursion.

Is my code clean and readable?

For the most part yes! Nice code, you got docstrings for your public methods, you respect PEP8 it's all good. Maybe you could add documentation for the private method as well? Especially for hard stuff like _siftdown and _siftup.

Just a few things:

  • the ternary I changed in _siftdown I consider personally really hard to read.

  • comparer seems like a French name, why not compare? Either I missed something or you mixed language and you shouldn't.

Do my test suffice (for say an interview)?

I'd say no. Use a module to do unit testing. I personally like pytest.

You prefix the name of your testing file by test_ and then your tests methods are prefixed/suffixed by test_/_test. Then you just run pytest on the command line and it discovers tests automatically, run them and gives you a report. I highly recommend you try it.

Another great tool you could have used is hypothesis which does property-based testing. It works well with pytest.

An example for your case:

from hypothesis import given, assume
import hypothesis.strategies as st

@given(st.lists(st.integers()))
def test_minheap(l):
    h = MinHeap.createHeap(l)
    s = sorted(l)
    for i in range(len(s)):
        assert(h.pop() == s[i])
        
@given(st.lists(st.integers()))
def test_maxheap(l):
    h = MaxHeap.createHeap(l)
    s = sorted(l, reverse=True)
    for i in range(len(s)):
        assert(h.pop() == s[i])

It pretty much gives the same kind of testing you did in your automatic_test but gets a bunch of cool feature added, and is shorter to write.

Raymond Hettinger did a really cool talk about tools to use when testing on a short time-budget, he mention both pytest and hypothesis, go check it out :)

Is the usage of subclasses MinHeap and MaxHeap & their comparer method that distincts them, a good approach to provide both type of heaps?

I believe it is! But speed wise, you should instead redeclare siftdown and siftup in the subclasses and replace instance of compare(a,b) by a < b or a > b in the code.

End note

Last thing is a remark, on wikipedia, the article say:

sift-up: move a node up in the tree, as long as needed; used to restore heap condition after insertion. Called "sift" because node moves up the tree until it reaches the correct level, as in a sieve.

sift-down: move a node down in the tree, similar to sift-up; used to restore heap condition after deletion or replacement.

And I think you used it in this context but on the heapq module implementation it seems to have the name backward?

They use siftup in pop and siftdown in push while wikipedia tells us to do the inverse. Somebody can explain please?

(I asked this question on StackOverflow, hopefully I'll get a response)