pandas nullable Int64 NA comparison gotchas
Explains pandas nullable Int64 NA comparison gotchas: why NA comparisons return NA instead of True/False and how to write filters that behave. Use when boolean masks with Int64 columns produce NA and break filtering, when NA == NA comparisons surprise you, or when groupby and sorting misbehave around NA. Not for float NaN semantics, for copy-on-write assignment issues, or for Arrow string migration.
TL;DR
Comparisons involving pd.NA return pd.NA, not True or False, so build masks with .isna() / .notna() explicitly and use fillna before comparing when you need a plain boolean. Nullable Int64 propagates missingness where float64 quietly coerced it.
pandas nullable Int64 NA comparison gotchasUse this when
- A boolean mask on an Int64 column contains NA and filtering breaks
NA == NAdoes not behave like you expected- Sorting or groupby acts oddly around missing integers
Not for this skill when
- You are dealing with float NaN, which follows different (numpy) rules
- Chained assignment silently does nothing
- You are migrating string columns to Arrow dtype
Steps
- See the gotcha in action so you stop trusting the comparison:
s = pd.Series([1, None, 3], dtype="Int64")
print(s == 1)
print((s == 1).tolist())Expected output: [True, NA, False]. The NA row is neither True nor False, and boolean indexing with NA in the mask raises.
- Write NA-safe filters by combining the comparison with an explicit NA check:
mask = s.eq(1).fillna(False)
print(s[mask].tolist())Expected output: [1]. fillna(False) turns the undecided NA into False, giving you a clean boolean mask.
- When you need NA to count as equal, say so explicitly:
print(s.eq(1) | s.isna())Expected output: [True, True, False]. There is no comparison operator that treats NA as equal to a value, you have to OR in the isna() branch.
- For aggregations, decide what NA means before you aggregate:
print(s.sum()) # skips NA by default
print(s.sum(min_count=1)) # NA-aware variantExpected output: 4 for the plain sum (NA skipped). Check skipna on every aggregation you use in a pipeline, the default is skip but not everywhere.
- Sort and group deliberately around NA:
print(s.sort_values(na_position="first").tolist())
print(s.value_counts(dropna=False).to_dict())Expected output: NA placement is explicit in the sort, and value_counts shows the NA bucket when dropna=False. Defaults hide NA, which is how it leaks into downstream joins.
Variant phrasings
pandas Int64 boolean indexing NA error
Filtering with a mask that contains NA raises. Apply step 2: fillna(False) on the mask before indexing.
pd.NA == pd.NA returns NA
Correct and by design. NA means unknown, and unknown == unknown is unknown. Use .isna() on both sides when you need to match missings.
nullable integer groupby dropping NA keys
groupby drops NA keys by default. Pass dropna=False to keep the NA group visible.
Why it happens
Nullable Int64 uses pd.NA with three-valued Kleene logic: any comparison with NA yields NA. Float columns historically used NaN, where nan == nan is False but the mask at least stays boolean. The nullable dtypes are stricter and more correct, which means code written against float-NaN habits breaks in new ways.
Edge cases
s == sis not all-True when NA is present, never use self-equality to find duplicates on nullable columns.- Merging on nullable Int64 keys: NA keys never match, same as NaN.
astype("Int64")on a float column with NaN converts NaN to NA silently, audit downstream comparisons.fillnawith a value outside the int range upcasts the column, check the dtype after filling.
Provenance
Resolved from the public thread: https://vectle.com/posts/pstfRUjrc1LZ0D6QgvxL23jA