NaN Is Weird

2026-03-0818:083570brassnet.biz

In which I showcase a Python oddity surrounding NaN and dictionaries.

Published March 02, 2026

Last week in the Python Discord we had an unusual discussion about a Python oddity. Specifically concerning float('nan'). It turns out that (rather unsurprisingly when you think about it) float('nan') is hashable.


>>> hash(float('nan'))
274121555

If it is hashable you can put it in a set:


>>> set(float('nan') for _ in range(10))
{nan, nan, nan, nan, nan, nan, nan, nan, nan, nan}

But why are there 10 copies of nan in that set??? A set shouldn't contain duplicates but that set obviously does. This comes down to the simple fact that no two instances of nan equal one another:


>>> float('nan') == float('nan')
False

In fact, the same nan doesn't even equal itself!


>>> nan = float('nan')
>>> nan == nan
False
>>> nan is nan
True

Just to make life even more fun, you can use nan as a key in a dictionary:


>>> my_dict = {float('nan'): 1, float('nan'): 2}
>>> my_dict
{nan: 1, nan: 2}

But, of course, you can't actually get to those values by their keys:


>>> my_dict[float('nan')]
Traceback (most recent call last): File "", line 1, in  my_dict[float('nan')] ~~~~~~~^^^^^^^^^^^^^^
KeyError: nan 

That is, unless you stored the specific instance of nan as a variable:


>>> my_dict[nan] = 3
>>> my_dict[nan]
3

You can always get the keys from the dictionary and work that way, but who knows if you'll get the nan you're looking for:


>>> my_keys = list(my_dict.keys())
>>> my_dict[my_keys[0]]
1

As a bonus, we can't even get an accurate count of how many times nan appears in an iterable...


>>> from collections import Counter
>>> Counter(float('nan') for _ in range(10))
Counter({nan: 1, nan: 1, nan: 1, nan: 1, nan: 1, nan: 1, nan: 1, nan: 1, nan: 1, nan: 1})

While I can't see myself ever, deliberately, using nan as a dictionary key, it is a fun little Python oddity.

Previous: Picking talks for PyCon US 2026 Next: Let's go to Ohio!



Read the original article

Comments

  • By cmovq 2026-03-1220:351 reply

    > we had an unusual discussion about a Python oddity

    There are so many discussions about "X language is so weird about it handles numbers!" and it's just IEEE 754 floats.

    • By swiftcoder 2026-03-1220:432 reply

      The oddity here is not the float itself, it's that Python provided a default hash implementation for floats

      • By JuniperMesos 2026-03-1220:531 reply

        Yeah IEEE 754 floating point numbers should probably not be hashable, and the weird (but standard-defined) behaviour with respect to NaN equality is one good reason for this.

      • By _cogg 2026-03-1221:571 reply

        Python supports arithmetic on mixed numeric types, so it makes sense that floats and ints should have a hash function that behaves somewhat consistently. I don't write a lot of python, but having used other scripting languages it wouldn't surprise me if numeric types get mixed up by accident often enough. You probably want int(2) and float(2) to be considered the same key in a dictionary to avoid surprises.

        See: https://docs.python.org/3/library/stdtypes.html#hashing-of-n...

        • By swiftcoder 2026-03-137:371 reply

          > You probably want int(2) and float(2) to be considered the same key in a dictionary to avoid surprises

          There are a variety of problems here:

          - floats can't represent whole numbers exactly as they get further from zero.

          - error accumulates in floating point arithmetic, so calculated keys may not match constant keys

          - Do you really want 2.0 and 2.00001 to refer to different objects?

          • By happytoexplain 2026-03-1313:181 reply

            >floats can't represent whole numbers exactly as they get further from zero.

            >error accumulates in floating point arithmetic, so calculated keys may not match constant keys

            Yes, but the parent used a toy example. It's a programming fundamental that you shouldn't be converting magic numbers to floats or doing math with floats without considering inaccuracy. These problems apply everywhere programmers use floats - they must understand them, regardless of whether they are using them as hash keys or any other purpose.

            >Do you really want 2.0 and 2.00001 to refer to different objects?

            Yes, very much so. They are different values. (Assuming you are using 2.00001 as shorthand for "a float close to 2")

            The advantage of common primitives being hashable is theoretically very high. In Swift (where NaN hashes to a different value every time, since that makes sense), hashable primitives makes hashability composable, so anything composed of hashable parts is hashable. And hashability is important not just for storage in maps, but all sorts of things (e.g. set membership, comparison for diffs, etc).

            The upsides of floats being hashable is potentially much higher than the downsides, which mostly stem from a programmer not understanding floats, not from floats being hashable.

            • By swiftcoder 2026-03-1314:31

              > It's a programming fundamental that you shouldn't be converting magic numbers to floats or doing math with floats without considering inaccuracy.

              This is all well and good in a language that makes you declare distinct floating point types, but in python things are maybe not so clear cut - the language uses arbitrary precision integers by default, and it's not always crystal clear when they are going to be converted to a (lossy) floating point representation

              Yeah, the programmer should probably be aware of the ins and out here, but python folks often aren't all that in the weeds with the bits and the bytes

  • By AndriyKunitsyn 2026-03-1220:303 reply

    NaN that is not equal to itself _even if it's the same variable_ is not a Python oddity, it's an IEEE 754 oddity.

    • By riskassessment 2026-03-1220:522 reply

      Nor is that inequality an oddity at all. If you were to think NaN should equal NaN, that thought would probably stem from the belief that NaN is a singular entity which is a misunderstanding of its purpose. NaN rather signifies a specific number that is not representable as a floating point. Two specific numbers that cannot be represented are not necessarily equal because they may have resulted from different calculations!

      I'll add that, if I recall correctly, in R, the statement NaN == NaN evaluates to NA which basicall means "it is not known whether these numbers equal each other" which is a more reasonable result than False.

      • By AndriyKunitsyn 2026-03-1221:131 reply

        It's the only "primitive type" that does that. If I deserialize data from wire, I'll be very surprised when the same bits deserialize as unequal variables. If it cannot be represented, then throwing makes more sense than trying to represent it.

        • By adrian_b 2026-03-1221:38

          Other primitive types also do this, but this is not clearly visible from high-level programming languages, because most HLLs have only incomplete support for the CPU hardware.

          If you do a (signed) integer operation, the hardware does not fit the result in a register of the size expected in a HLL, but the result has some bits elsewhere, typically in a "flags" register.

          So the result of an integer arithmetic operation has an extra bit, usually named as the "overflow" bit. That bit is used to encode a not-a-number value, i.e. if the overflow bit is set, the result of the operation is an integer NaN.

          For correct results, one should check whether the result is a NaN, which is called checking for integer overflow (unlike for FP, the integer execution units do not distinguish between true overflow and undefined operations, i.e. there are no distinct encodings for infinity and for NaN). After checking that the result is not a NaN, the extra bit can be stripped from the result.

          If you serialize an integer number for sending it elsewhere, that implicitly assumes that wherever your number was produced, someone has tested for overflow, i.e. that the value is not a NaN, so the extra bit was correctly stripped from the value. If nobody has tested, your serialized value can be bogus, the same as when serializing a FP NaN and not checking later that it is a NaN, before using one of the 6 relational operators intended for total orders, which may be wrong for partial orders.

      • By themafia 2026-03-1221:27

        > "it is not known whether these numbers equal each other"

        Equality, among other operations, are not defined for these inputs. NaN's really are a separate type of object embedded inside another objects value space. So you get the rare programmers gift of being able to construct a statement that is not always realizable based solely on the values of your inputs.

    • By paulddraper 2026-03-1221:41

      It's an IEEE-754 oddity that Python chose to adopt for its equality.

      IEEE-754 does remainder(5, 3) = -1, whereas Python does 5 % 3 = 2.

      There's no reason to expect exact equivalence between operators.

    • By adrian_b 2026-03-1221:153 reply

      It is not an IEEE 754 oddity. It is the correct mathematical behavior.

      When you define an order relation on a set, the order may be either a total order or a partial order.

      In a totally ordered set, there are 3 values for a comparison operation: equal, less and greater. In a partially ordered set, there are 4 values for a comparison operation: equal, less, greater and unordered.

      For a totally ordered set you can define 6 relational operators (6 = 2^3 - 2, where you subtract 2 for the always false and always true predicates), while for a partially ordered set you can define 14 relational operators (14 = 2^4 - 2).

      For some weird reason, many programmers have not been taught properly about partially-ordered sets and also most programming languages do not define the 14 relational operators needed for partially ordered sets, but only the 6 relational operators that are sufficient for a totally ordered set.

      It is easy to write all 14 relational operators by combinations of the symbols for not, less, greater and equal, so parsing this in a programming language would be easy.

      This lack of awareness about partial order relations and the lack of support in most programming languages is very bad, because practical applications need very frequently partial orders instead of total orders.

      For the floating-point numbers, the IEEE standard specifies 2 choices. You can either use them as a totally-ordered set, or as a partially-ordered set.

      When you encounter NaNs as a programmer, that is because you have made the choice to have partially-ordered FP numbers, so you are not allowed to complain that this is an odd behavior, when you have chosen it. Most programmers do not make this choice consciously, because they just use the default configuration of the standard library, but it is still their fault if the default does not do what they like, but nonetheless they have not changed the default settings.

      If you do not want NaNs, you must not mask the invalid operation exception. This is actually what the IEEE standard recommends as the default behavior, but lazy programmers do not want to handle exceptions, so most libraries choose to mask all exceptions in their default configurations.

      When invalid operations generate exceptions, there are no NaNs and the FP numbers are totally ordered, so the 6 relational operators behave as naive programmers expect them to behave.

      If you do not want to handle the invalid operation exception and you mask it, there is no other option for the CPU than to use a special value that reports an invalid operation, and which is indeed not-a-number. With not-numbers added to the set of FP numbers, the set becomes a partially-ordered set and all relational operators must be interpreted accordingly.

      If you use something like C/C++, with only 6 relational operators, then you must do before any comparison tests to detect any NaN operand, because otherwise the relational operators do not do what you expect them to do.

      In a language with 14 relational operators, you do not need to check for NaNs, but you must choose carefully the relational operator, because for a partially-ordered set, for example not-less is not the same with greater-or-equal (because not-less is the same with greater-or-equal-or-unordered).

      If you do not expect to do invalid operations frequently, it may be simpler to unmask the exception, so that you will never have to do any test for NaN detection.

      • By AndriyKunitsyn 2026-03-1221:541 reply

        >With not-numbers added to the set of FP numbers, the set becomes a partially-ordered set and all relational operators must be interpreted accordingly.

        The same not-number, produced by the same computation, occupying the same memory, is still not equal to itself. It is true that I haven't been able to brush up my knowledge on partial ordering, but isn't being identical is the same as being equal in math?

        • By adrian_b 2026-03-137:19

          If you test if a not-a-number is equal to itself, the result is false.

          If you test if a not-a-number is not equal to itself, the result is also false.

          The reason is that the result of comparing a not-number with itself is neither "equal" nor "not equal", but "unordered".

          This should make perfect intuitive sense, because a NaN encodes the fact that "an undefined operation has happened".

          For instance, you have 2 instances of the same NaN, both having been obtained by multiplying zero with infinity.

          However it may happen that one was obtained while computing values of a sequence that converges to 10 and the other may have been obtained by computing values of a sequence that converges to 100.

          Then there is no doubt that equality is not true for this 2 NaNs.

          However, those 2 NaNs may have been generated while computing values of sequences that both converge to 10, which shows that neither non-equality may be trued for these 2 NaNs.

          When you have NaNs, that means that it is unknown which value, if any, the invalid operations should have produced.

          When comparing 2 unknown values, you cannot know whether they are equal or not equal, so both testing for equality and for non-equality must fail.

      • By xigoi 2026-03-137:26

        This has nothing to do with partial orders. Mathematically, a value is always equal to itself, no matter if it’s from a partially ordered set.

      • By jcranmer 2026-03-1223:491 reply

        You can define a perfectly rational partial order on floats that make NaNs unordered with respect to numbers but equal to other NaNs.

        • By adrian_b 2026-03-137:341 reply

          No, you cannot, because that is logically inconsistent and it leads to bugs.

          NaN means either an unknown number or that no number can satisfy the condition of being the result of the computed function.

          When you compare unknown numbers, you cannot know if they are equal and you cannot know if they are not equal.

          That is why both the equality operator and the non-equality operator must be false when comparing a NaN with itself.

          When you see a binary FP number, the encoding no longer has any memory about where the number has been generated.

          For deciding that a NaN is equal to itself, it is not enough to examine the encoding, you must know the history of the 2 instances of the same NaN value, where they had been generated. Only if the 2 instances are duplicates of a single value, i.e. they had been generated by the same operation, then you could say that they are equal.

          If you would want such a facility in a program, then it is not enough to propagate just a floating-point value. You would have to define a custom data type and accompany the FP value by another value that encodes its provenance.

          Then you could make custom equality and non-equality operators, which when comparing identical NaNs also compare their provenances, and they decide whether the NaNs are equal or non-equal based on the provenances.

          Such a use case is extremely rare. In decades of experience I have never seen a situation when such a feature would be desirable. Nevertheless, if someone wants it, it would be easy to implement it, but it will add considerable overhead.

          For example, the provenance could be encoded as a pointer to strings of the form "File ... Line ...", produced using the normal compiler facility for this, which will refer to the source line where the result of a function is tested for being a NaN, and if so the provenance pointer is stored, instead of a null pointer.

          Even this encoding of the provenance may not be foolproof, because some functions may generate NaNs with different meanings. For a complete provenance encoding, one would need not only a pointer identifying the function that had generated the NaN, but also the value of a static counter that is incremented at each function invocation.

          The provenance could be encoded compactly by storing the string pointers in an array and storing in the provenance an index into that array together with the value of the invocation counter.

          So it can be done, but I am pretty sure that this would never be worthwhile.

          • By jcranmer 2026-03-1313:26

            Recall for a minute that floating-point arithmetic is inherently inexact, and it is thus pretty rare that you query for equality using an == operator rather than a relative or absolute error check (both of which involve < or > against a non-NaN value and thus would fail anyways for NaN). No, the main reason to ever actually use == on floating-point is if you're doing a bunch of tests to make sure expressions get the correct exact result (so you're expecting the inexactness to resolve in a particular way) or if you're doing non-computational stuff with floats (such as using them as keys in a map), at which point it becomes a problem that x != x is ever true.

            > If you would want such a facility in a program, then it is not enough to propagate just a floating-point value. You would have to define a custom data type and accompany the FP value by another value that encodes its provenance.

            IEEE 754 was way ahead of you. The entire rationale for the existing of NaN payloads was to be able to encode the diagnostic details about operation failure. And this suggests that you really want is the rule that NaNs are equal only to other NaNs with the same payload but are unordered with respect to everything else.

            From the research I've done, it seems that the main reason that x != x for NaNs isn't some deep theoretical aspect about the properties of NaNs, but rather because IEEE 754 wanted a cheap way to test for NaN without having to call a function.

  • By agwa 2026-03-1220:241 reply

    Fun fact - in C++ std::sort has undefined behavior, and can crash[1], if you try to sort a container with NaNs in it.

    [1] https://stackoverflow.com/questions/18291620/why-will-stdsor...

    • By tialaramex 2026-03-1223:14

      This particular defect is a Quality of Implementation problem that's cheerfully allowed by the standard.

      Rust's standard sorts not only lack the API footgun which causes so many C++ programmers to blow their feet off this way, but even if you go out of your way to provide a nonsensical comparison both sorts are safe anyway because it's a safe language and yet they're also faster than any popular implementation of std::sort / std::stable_sort as appropriate.

      It's kinda silly, as late as the Biden administration one of the popular C++ stdlib implementations wasn't even O(n log n) because apparently twenty years was not enough for them to have introduced an introsort yet... That one isn't the standard's fault, the C++ 11 standard does say you should provide an O(n log n) sort at least.

HackerNews