skip to content

Sorting and Ordered Access

Getting elements out in the order you need: sorting with key functions, holding a list sorted with bisect, pulling smallest-first with heapq. Interviewers watch whether you reach for the stdlib first.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

12

Python's heapq is a min-heap - how do you get max-heap behaviour from it?

level: juniorimportance: must knowfreq 70%

answer

  1. Functions over a list, not a class
  2. Only one end is guaranteed
  3. Sign arithmetic on the ordering key
  4. 3.14 added mirrored function names
  5. One-shot ranking has its own helper

basics

~20 s

heapq keeps the smallest item at index 0 and has no reverse switch. Negate the ordering key on the way in and back on the way out, or on Python 3.14 call its max-heap functions such as heapify_max and heappop_max.

solid answer

~40 s

`heapq` is not a class — it is a set of functions that maintain the min-heap invariant over an ordinary `list`, so `heap[0]` is always the smallest element and `heapq.heappop` always returns that one. There is no `reverse=` parameter anywhere in the module. Three routes give you largest-first access. The portable one is sign negation: push `-value`, or `(-priority, item)`, and negate again after popping — arithmetic only, so it works for numbers and fails for `str` or `datetime`. For a one-shot answer, `heapq.nlargest` reads the whole iterable and hands back the biggest items directly. Since **Python 3.14** the module also exposes real max-heap functions — `heapify_max`, `heappush_max`, `heappop_max`, `heapreplace_max`, `heappushpop_max` — which maintain the mirrored invariant over the same plain list. Never mix the two families on one list.

code

python · 12 lines
python
import heapq

# Portable: push the negated key, negate again on the way out
h = []
for score in (5, 1, 9):
    heapq.heappush(h, -score)
print(-heapq.heappop(h))

# Python 3.14: real max-heap helpers over the same plain list
m = [5, 1, 9]
heapq.heapify_max(m)
print(m[0], heapq.heappop_max(m))

go deeper

for a junior

Be ready to say that heapq is a set of functions over an ordinary list, that heap[0] is the smallest element, and that reversing the order means negating the key or, on 3.14, calling the max-heap functions.

for a middle

Explain that heapify mutates in place and returns None, that the invariant covers index 0 only, and why negation breaks for non-numeric keys. Know when nlargest replaces a heap entirely.

for a senior

Show the judgement of picking a live heap over a one-shot ranking, and flag the real hazard: nothing validates the invariant, so a list mutated behind heapq's back yields wrong answers silently rather than raising.

for a principal

Own the portability call. Committing to the 3.14 max-heap functions sets an interpreter floor for the whole codebase; negation keeps you portable at the cost of an encoding every reader must decode. Decide once and write it down.

### There is no heap object The first thing to say out loud in an interview is that `heapq` gives you *functions*, not a type. `heapq.heapify(data)` rearranges the existing `list` in place and returns `None`; `heapq.heappush(data, item)` appends and restores the invariant; `heapq.heappop(data)` removes and returns `data[0]`. The container stays a plain `list` the whole time, which is why you can `len()` it, slice it, pickle it or pass it to a function that has never heard of `heapq`. It also means nothing stops you from mutating that list behind the module's back — `data.append(x)` or `data.sort(reverse=True)` leaves an object that still looks like a list but no longer satisfies what the next `heappop` assumes, and you get silently wrong results rather than an exception. The invariant the module maintains is one-sided: **the smallest element is at index 0**. Nothing else is ordered. A common beginner claim is that `heapify` sorts the list, or that `data[-1]` is the maximum. Neither is true; only position 0 carries a guarantee. ### Why min-only, and the three ways around it The module was written around a single ordering direction and, until recently, exposed only that. So for largest-first access you pick one of three tools. **1. Negation.** Push the arithmetic negative of the ordering key and undo it on the way out: ```python import heapq largest_first = [] for score in (5, 1, 9): heapq.heappush(largest_first, -score) top = -heapq.heappop(largest_first) # 9 ``` With payloads you negate only the priority field of the entry tuple: `heapq.heappush(h, (-priority, item))`. The limitation is that this is real arithmetic. `-"gene"` and `-datetime.date.today()` both raise `TypeError`, so for non-numeric orderings you either derive a numeric surrogate (a negated POSIX timestamp, a negated code point sequence) or wrap the value in a small class whose `__lt__` is inverted. **2. `heapq.nlargest` / `heapq.nsmallest`.** When you want an answer rather than a live structure, `heapq.nlargest(3, data)` and its `key=` parameter give you the result directly and you never touch a heap at all. This is the right call for a one-pass question over a finite iterable; it is the wrong call when items keep arriving and you must pop as you go. **3. The 3.14 max-heap functions.** Python 3.14 promoted the module's long-standing private max-heap helpers to public names: `heapify_max`, `heappush_max`, `heappop_max`, `heapreplace_max` and `heappushpop_max`. They maintain the mirror invariant — largest at index 0 — over the same ordinary list: ```python import heapq readings = [5, 1, 9] heapq.heapify_max(readings) readings[0] # 9 heapq.heappop_max(readings) # 9 ``` On 3.13 and earlier those names do not exist; code that must run on older interpreters uses negation. And the two families are not interchangeable on one list: a list built with `heappush` satisfies the min invariant, and calling `heappop_max` on it returns garbage without complaining. ### What interviewers are really checking Three things. First, that you know the container is a list and the module is a set of functions over it — candidates who reach for a nonexistent `heapq.Heap()` or pass `reverse=True` have never used it. Second, that you can state the invariant precisely: index 0 only. Third, that you pick the right one of the three tools for the shape of the problem — a live priority structure wants a heap, a one-shot ranking wants `nlargest`, and a portable max-heap on an older interpreter wants negation. A useful closing detail: `heapq.heapify` works on any list you already have, so the usual pattern is to build the list however you like and heapify once, rather than pushing elements one at a time. And if you find yourself needing both directions over the same data at once, that is a signal you want two structures, not one clever encoding.

  • Why does the negation trick fail for a heap keyed on strings or dates?
    Because negation is arithmetic, not ordering. `str` and `datetime` objects have no `__neg__`, so `-value` raises `TypeError`. Either derive a numeric surrogate you can negate — a negated timestamp, for instance — or wrap each entry in a small class whose `__lt__` compares the other way round, or use the 3.14 max-heap functions.
  • What happens if you call heapq.heappop on a list you never heapified?
    Nothing raises. The functions trust the invariant rather than verifying it, so on an arbitrary list `heappop` returns element 0 — whatever happened to be first — and then rearranges the rest under an assumption that never held. You get plausible, wrong answers. Always `heapq.heapify` a pre-existing list before treating it as a heap.
  • Can you keep both a min-heap and a max-heap view over the same list object?
    No. The two function families maintain incompatible invariants at index 0, so a list can satisfy only one at a time. If you need both directions, keep two lists holding the same items — which is what a two-structure design does — and accept the duplicated storage and the synchronisation cost.

The module is a set of tools for keeping one list tidy at one end, like a spike file that always shows the earliest docket on top - to see the latest first you either file everything with the dates flipped or use a second spike that stacks the other way.

saying these in an interview costs you the question

  • Claims heapq exports a Heap or MaxHeap class
  • Passes reverse=True to heapify or heappush
  • Says the list is fully sorted after heapify
  • Assumes heap[-1] holds the largest element
  • Expects heapify to return a new heap object
  • Tries to negate a string to reverse the order

context

open as a page

What is the difference between sorted() and list.sort() in Python?

level: juniorimportance: must knowfreq 85%

basics

~20 s

sorted() takes any iterable and returns a new sorted list, leaving the original untouched. list.sort() reorders an existing list in place and returns None. Both run the same stable sort and accept the same key and reverse arguments.

open as a page

How do bisect.bisect_left and bisect.bisect_right differ on duplicate values?

level: middleimportance: must knowfreq 55%

basics

~20 s

Both return an index where the value could be inserted keeping the list sorted. On a run of equal values bisect_left returns the index before the run and bisect_right just after it, bracketing the duplicates.

open as a page

Why can heapq.heappush raise TypeError when two entry tuples tie on priority?

level: middleimportance: must knowfreq 55%

basics

~20 s

Tuples compare element by element. When two priorities are equal, Python moves on to the next field and compares the payloads themselves; if those are dicts or plain objects with no ordering, the comparison raises TypeError.

open as a page

What does sorted()'s key argument receive, and how many times is it called?

level: middleimportance: must knowfreq 70%

basics

~20 s

key is a one-argument callable applied to each element exactly once, before any comparing happens; the sort then orders elements by those computed key values. For n elements it runs n times, not once per comparison.

open as a page

How does bisect.bisect_right turn a numeric score into a letter grade?

level: juniorimportance: should knowfreq 35%

basics

~10 s

Keep the bucket boundaries in one ascending list and the labels in a parallel sequence. bisect.bisect_right returns how many boundaries the score has passed, and that count is the index of its label.

open as a page

Why does bisect.insort slow down as an email-digest sender's pending list grows?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Finding the slot is logarithmic, but placing the item is not. A Python list stores its elements contiguously, so inserting in the middle shifts every later entry, making each insort cost time proportional to the list length.

open as a page

heapq.merge output in a nightly pipeline arrives out of order and downstream silently truncates - what went wrong?

level: seniorimportance: should knowfreq 28%

basics

~20 s

heapq.merge does not sort. It assumes every input iterable is already ordered, and it never checks. One unsorted source makes the merged stream go backwards, and any consumer that stops at the first out-of-order record truncates without error.

open as a page

Python's sorted() is stable, so why do tied records still swap order between runs of a 340-case regression pack?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Stability preserves the order equal keys arrived in, so it is only as deterministic as the input. A set or a directory listing hands the sort a different order each run. Fix it with a total key that leaves no ties.

open as a page

In bisect, how does the key= parameter added in Python 3.10 treat the x argument?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

The search functions apply key only to list elements, never to x, so you pass an already-extracted key such as 45. The insort functions are the exception: they apply key to the item being inserted.

open as a page

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

level: middleimportance: nice to knowfreq 25%

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.

open as a page

When do you need functools.cmp_to_key instead of a plain key function for sorting?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Only when the ordering rule is genuinely pairwise and cannot be expressed as a value computed from one element alone, or when porting a legacy two-argument comparator. functools.cmp_to_key wraps that comparator so sorted() can use it, at a real performance cost.

open as a page