skip to content

How do heapq.heappushpop and heapq.heapreplace differ on the same list?

level: middleimportance: nice to knowfreq 25%

answer

  1. Same two operations, opposite sequence
  2. One of them can return your own argument
  3. One of them can return something bigger
  4. Only one survives an empty list
  5. The documented guard duplicates the other function

basics

~20 s

The order of the two operations differs. heappushpop pushes first, so it can hand straight back the item you passed. heapreplace pops first, so it always evicts the current smallest, even if the new item is smaller.

solid answer

~40 s

Both combine an insert and a removal into one pass, and both return one element, but they are not interchangeable. `heapq.heappushpop(h, item)` conceptually pushes `item` and then pops the smallest, so when `item` is smaller than or equal to `h[0]` it is returned immediately and the heap is left untouched. `heapq.heapreplace(h, item)` pops the current root first and then pushes `item`, so it **always** returns the old smallest and always ends up holding `item` — the returned value may well be larger than the one you inserted. They also differ on an empty heap: `heappushpop([], x)` returns `x`, while `heapreplace([], x)` raises `IndexError`. That is why the stdlib documents `heapreplace` guarded by `if item > heap[0]`, which is precisely the comparison `heappushpop` already performs for you.

code

pycon · 11 lines
pycon
>>> import heapq
>>> h = [1, 3, 5]
>>> heapq.heapreplace(h, 0)
1
>>> h
[0, 3, 5]
>>> h2 = [1, 3, 5]
>>> heapq.heappushpop(h2, 0)
0
>>> h2
[1, 3, 5]

go deeper

for a junior

Recall that both functions do an insert and a removal in one call and leave the list the same length, and that heapreplace on an empty list raises IndexError while heappushpop does not.

for a middle

Explain the operation order precisely: push-then-pop can hand back your own argument, pop-then-push always evicts the current root and may return something larger than what you inserted.

for a senior

Show that swapping one for the other changes which element is discarded with no error and no length change, and use the docstring's if item > heap[0] guard to argue that the guarded heapreplace is just a hand-rolled heappushpop.

for a principal

Frame it as an API-selection standard: pick one of the two per use case, write down which and why, and treat the fill phase before the collection reaches its target size as an explicit branch rather than something the choice of function papers over.

### Two names, one operation, opposite order Both functions exist because doing an insert and a removal as two separate calls does more work than doing them together, and because they cover two different intents. `heapq.heappushpop(heap, item)`: push `item`, then pop and return the smallest. If `item` is already smaller than the current root, pushing it would make it the new root, and popping would immediately take it back out — so the implementation short-circuits, returns `item`, and leaves the list exactly as it was. `heapq.heapreplace(heap, item)`: pop and return the current smallest, then push `item`. There is no short-circuit and no comparison against `item`. The root leaves, `item` goes in, unconditionally. The difference shows up the moment the incoming item is small: ```python import heapq h = [1, 3, 5] heapq.heapreplace(h, 0) # returns 1, heap becomes [0, 3, 5] h2 = [1, 3, 5] heapq.heappushpop(h2, 0) # returns 0, heap stays [1, 3, 5] ``` Same list, same argument, different survivor. Swap one call for the other in code that maintains a bounded collection of the best-so-far and you have changed which element gets discarded — without any error, any warning, or any change in the list's length. ### The docstring's warning, decoded The `heapreplace` docstring says outright that "the value returned may be larger than item" and that this "constrains reasonable uses of this routine unless written as part of a conditional replacement": ```python if item > heap[0]: item = heapq.heapreplace(heap, item) ``` Read that guard carefully: it is the comparison `heappushpop` performs internally. If your guard is exactly this, `heappushpop` is the same operation with one fewer place to make a mistake. `heapreplace` earns its keep when you *want* unconditional eviction — a rolling window where the oldest entry must leave no matter what replaces it, or a case where you have already decided the root is going, independent of the newcomer's value. ### The empty-heap divergence The second, sharper difference: ```python heapq.heappushpop([], 4) # 4 heapq.heapreplace([], 4) # IndexError: index out of range ``` `heapreplace` must pop first, and there is nothing to pop. `heappushpop` pushes first, so there always is. This bites in warm-up code: a loop that calls `heapreplace` from the first iteration crashes on iteration one, while the same loop written with `heappushpop` quietly does the wrong thing instead — it accepts the first item and returns it, so the heap never fills. Neither is a substitute for handling the fill phase explicitly, usually with `heapq.heappush` until the collection reaches its target size and one of these two afterwards. ### Why they exist at all Each performs a single restore pass rather than the two that a separate `heappush` plus `heappop` would do, and neither changes the length of the list. That last property is what makes them the natural pair of calls for anything that must stay a fixed size: the list length is an invariant of the loop rather than something you re-check. One more detail worth knowing: Python 3.14 added `heappushpop_max` and `heapreplace_max` alongside the other max-heap functions, with the same relationship to each other, mirrored. As with all of that family, do not mix them with the min-heap functions on one list — each maintains its own invariant and neither validates the other's. ### How to answer State the operation order, give the small-item example where the return values differ, mention the empty-heap `IndexError`, and finish with the guard from the docstring and the observation that the guard *is* `heappushpop`. That last line is what separates someone who has read the module from someone who has only used it.

  • The heapreplace docstring shows it guarded by `if item > heap[0]`. Why is that guard there?
    Because `heapreplace` evicts the root unconditionally, even when the item you are inserting is smaller than the one being thrown away — which is almost never what a best-so-far collection wants. The guard restores the missing comparison. And since that comparison is exactly what `heappushpop` does internally, the guarded form is a hand-written `heappushpop`.
  • Can heapq.heappushpop return an item that was already in the heap?
    Yes. When the pushed item is greater than the current root, the root is evicted and returned while the pushed item stays. You only get your own argument back in the opposite case, where it is smaller than or equal to the root and the heap is left unchanged.
  • Do either of these functions change the length of the list?
    No. Both perform one insert and one removal, so the list ends the call at exactly the length it started — which is what makes them the natural calls for a fixed-size collection. Growing the collection needs `heapq.heappush`, and shrinking it needs `heapq.heappop`.

saying these in an interview costs you the question

  • Says the two functions are aliases for each other
  • Expects heapreplace to keep the smaller of the two values
  • Calls heapreplace on a heap that may still be empty
  • Thinks either call grows the list by one element
  • Believes both functions return the new root

context