heapq: A Single NaN Silently Corrupts the Whole Heap Invariant (tested)
heapq maintains its invariant with comparisons like child < parent, which only works if every element in the heap is comparable with every other element under a consistent total order. float('nan') breaks that assumption: nan < x and x < nan are both always False, for every x, including nan itself. We measured what one stray NaN does to heapify(), heappush() and heappop() on otherwise-ordinary numeric data.
1. One NaN in the input is enough -- with no error and no warning
import heapq, math, warnings
warnings.simplefilter("error") # turn warnings into exceptions, to prove none fire
data = [5, 3, math.nan, 1, 4, 2]
heapq.heapify(data)
print("heapified:", data)
print("popped: ", [heapq.heappop(data) for _ in range(len(data))])
heapified: [1, 3, 2, 5, 4, nan]
popped: [1, 2, nan, 3, 4, 5]
Measured: the call ran to completion with warnings.simplefilter("error") active, so if Python had raised so much as a warning it would have become a visible exception -- it didn't. The result isn't just "NaN ends up somewhere odd": nan pops out before 3, 4 and 5, which makes the whole sequence not sorted by any definition, silently.
2. The corruption isn't limited to NaN -- other values get misordered too
import heapq, math
data = [28, 42, 32, 6, math.nan, 10]
h = list(data)
heapq.heapify(h)
popped = [heapq.heappop(h) for _ in range(len(h))]
non_nan = [x for x in popped if not (isinstance(x, float) and math.isnan(x))]
print("popped: ", popped)
print("non-NaN pop order: ", non_nan)
print("correct sorted order:", sorted(x for x in data if x == x))
popped: [10, 28, 32, 6, nan, 42]
non-NaN pop order: [10, 28, 32, 6, 42]
correct sorted order: [6, 10, 28, 32, 42]
Measured: 6 comes out after 32 -- a comparison that never even involves nan directly. One unorderable element anywhere in the data silently poisons the sift-up/sift-down logic enough to misplace unrelated real numbers too, not just the NaN itself.
3. Removing the NaN doesn't undo the damage already done
import heapq, math
data = [28, 42, 32, 6, math.nan, 10]
h = list(data)
heapq.heapify(h)
first = heapq.heappop(h) # happens to be the correct minimum, 10
rest = [heapq.heappop(h) for _ in range(len(h))]
print("first pop:", first)
print("remaining pops:", rest)
first pop: 10
remaining pops: [28, 32, 6, nan, 42]
Measured: the very first pop happened to return the correct minimum -- but the heap was already structurally corrupted by the earlier heapify(), so every pop after it is still wrong (6 still surfaces after 32). nan is still sitting unresolved in the middle of the structure; nothing about popping other elements around it repairs the invariant. The moment a NaN is sifted into a heap, the breakage outlives any single comparison involving it.
Frequently asked questions
Does heapq raise an error or warning if you heapify a list containing NaN?
No -- we measured it with warnings.simplefilter('error') active (which turns warnings into exceptions) around heapify(), heappush() and heappop() calls on data containing one float('nan'), and none of them raised anything.
If I put one NaN in my data, does only the NaN end up out of place?
No -- we measured a case where popping a heap containing [28, 42, 32, 6, nan, 10] returned the non-NaN values in the order [10, 28, 32, 6, 42], which is not sorted; 6 came out after 32 even though that comparison never involved NaN.
Can I fix a NaN-corrupted heap by just popping until the NaN comes out?
No -- we measured that popping elements before reaching the NaN doesn't repair the heap; the elements that pop out after it were still measurably out of order, because the structural corruption happened during heapify()/heappush(), not at pop time.
How do I avoid this?
Filter out NaN (and anything else that isn't comparable with your other data under a single total order, like mixing NaN into float data) before calling heapify() or heappush() -- heapq never checks this for you, and we measured zero errors or warnings when it's violated.
Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in heapq module: heapified and pushed lists containing one float('nan') among ordinary numbers, popped everything back out, and compared against sorted(); also ran the same calls with warnings.simplefilter('error') active to confirm nothing is raised. PlainHeaders is our extension; Python's heapq module is built in.