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
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.
# Pseudocode for dict lookup
def lookup(d, key):
bucket = hash(key) % table_size
for stored_key, stored_val in d._bucket(bucket):
if stored_key is key or stored_key == key: # identity FIRST
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
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
# Sets: same story
s = {nan1}
nan1 in s # True
nan2 in s # False
# 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:
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:
{float('nan'), float('nan'), float('nan')} # 3 distinct elements (each a different NaN)
But:
nan = float('nan')
{nan, nan, nan} # 1 element (same object)
How to detect NaN safely
Never use x == float('nan'). Use:
import math
math.isnan(x) # canonical test
x != x # works (only NaN is not equal to itself)
# pandas
import pandas as pd
pd.isna(x)
Interview angle
- 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.isnanorx != x.