float('nan') as a dict key

Updated 2 min read source
On this page7
  1. The gotcha
  2. Minimal repro
  3. Why it happens
  4. Variants
  5. What this affects
  6. How to detect NaN safely
  7. Interview angle

float('nan') as a dict key

The gotcha

NaN can be a dict key — but you can’t look it up by another NaN, because NaN is not equal to anything (including itself). The dict still holds the entry; the lookup just doesn’t find it.

You can look it up via the same NaN object you inserted, because CPython short-circuits identity (is) before equality (==).

Minimal repro

python
nan = float('nan')

d = {nan: "x"}
print(d[nan])                # "x"           — same object, works via identity
print(d[float('nan')])       # KeyError      — different NaN object, equality fails

# Verify:
nan == nan                   # False         ← IEEE 754 says NaN != NaN
nan is nan                   # True          ← but it's the same object

Why it happens

IEEE 754 defines NaN as not equal to anything. nan == nan is False in any sane numeric system. But for dict lookup, CPython optimizes: it tries key is stored_key first (cheap pointer compare), and only falls back to key == stored_key if identity fails.

python
# Pseudocode for dict lookup
def lookup(d, key):
    bucket = hash(key) % table_size
    for stored_key, stored_val in d._bucket(bucket):
        # identity FIRST
        if stored_key is key or stored_key == key:
            return stored_val
    raise KeyError(key)

Hash works because hash(float('nan')) is consistent — every NaN hashes the same. So all NaNs go to the same bucket. But the in-bucket comparison fails for different NaN instances.

Variants

python
import math
nan1 = float('nan')
nan2 = float('nan')
nan3 = math.nan

d = {nan1: 1}

d[nan1]    # 1                 same object → identity hit
d[nan2]    # KeyError           different object → equality fails
d[nan3]    # KeyError           different object
python
# Sets: same story
s = {nan1}
nan1 in s    # True
nan2 in s    # False
python
# In-list membership: also identity-first
[nan1].count(nan1)              # 1
[float('nan'), float('nan')]    # two distinct NaNs
[float('nan'), float('nan')].count(float('nan'))   # 0

What this affects

Pandas / NumPy treat NaN-as-missing carefully:

python
import pandas as pd
df = pd.DataFrame({"x": [1, float('nan')]})
df.x == df.x                    # [True, False]   ← NaN inequality leaks
df.x.equals(df.x)               # True            ← .equals treats NaN as equal
df.x.isna()                     # standard way to test for NaN

Set deduplication of NaN doesn’t work as expected:

python
# 3 distinct elements (each a different NaN)
{float('nan'), float('nan'), float('nan')}

But:

python
nan = float('nan')
# 1 element (same object)
{nan, nan, nan}

How to detect NaN safely

Never use x == float('nan'). Use:

python
import math
math.isnan(x)                   # canonical test
x != x                          # works (only NaN is not equal to itself)
python
# pandas
import pandas as pd
pd.isna(x)

Interview angle 4

  • Q: “Can you use float('nan') as a dict key?” — yes. The entry goes in.
  • Q: “Can you look it up?” — only with the same object, because NaN ≠ NaN.
  • Follow-up: “Why does d[nan] work then?” — CPython checks identity (is) before equality. Same NaN object shortcuts the equality check.
  • Follow-up: “How do you test for NaN safely?” — math.isnan or x != x.

See Floats and equality, __eq__ vs __hash__.