Pandas NA vs np.nan: Understanding Missing Value Differences in DataFrames

pd.NA is a dtype-agnostic missing value singleton introduced in pandas for nullable extension arrays, while np.nan is a float-specific IEEE-754 sentinel that only works with floating-point dtypes.

When working with missing data in the pandas-dev/pandas repository, understanding the distinction between these two sentinels is critical for correct data manipulation and type preservation. While both represent missing values, they differ fundamentally in their implementation, supported dtypes, and behavioral semantics across logical and arithmetic operations.

What Is pd.NA? The Pandas Missing Value Sentinel

pd.NA represents a missing value marker designed specifically for pandas' nullable extension dtypes. Unlike traditional missing value representations, it is not tied to any specific numeric type.

Implementation in pandas/_libs/missing.c

The core implementation resides in pandas/_libs/missing.c, where NAType is defined as a C-extension singleton type. This object is exposed to Python as pandas.NA through the top-level pandas/__init__.py import chain. The implementation ensures pd.NA is immutable and hashable, allowing it to function as a dictionary key when necessary.

Integration with Nullable Extension Dtypes

Each nullable extension dtype implements an na_value property returning libmissing.NA, defined in pandas/core/dtypes/base.py. When you create a Series with dtype Int64, boolean, or string, the underlying array implementations in pandas/core/arrays/integer.py, pandas/core/arrays/boolean.py, and pandas/core/arrays/string_.py store pd.NA in a separate mask array. This design keeps the data buffer in its native format (integer, boolean, etc.) while tracking missingness separately.

What Is np.nan? The NumPy Float Sentinel

np.nan (Not a Number) originates from NumPy's C implementation in numpy/core/_numpyinternal.c and represents an IEEE-754 floating-point value. It is specifically a float64 value, making it fundamentally different from pd.NA's type-agnostic design.

Because np.nan is a float, it only works naturally with pandas float dtypes. When placed in non-float columns, pandas coerces the entire column to object dtype, significantly impacting performance and memory usage.

Key Differences Between pandas NA and np.nan

Understanding the behavioral differences ensures you choose the appropriate sentinel for your data pipeline.

Type System and dtype Support

pd.NA works exclusively with pandas nullable extension dtypes (Int64, UInt64, Float64, boolean, string). These dtypes preserve the underlying data type while supporting missing values.

np.nan only functions correctly with float dtypes. Attempting to use np.nan in integer or boolean Series forces conversion to object dtype, losing type safety and vectorization benefits.

Equality and Comparison Behavior

The equality semantics differ fundamentally:

import pandas as pd
import numpy as np

# pd.NA equality

print(pd.NA == pd.NA)  # <NA> (returns NA, not True)

# np.nan equality

print(np.nan == np.nan)  # False

pd.NA propagates through comparisons, returning pd.NA when the result is indeterminate. np.nan follows IEEE-754 rules where it never equals itself, requiring np.isnan() for detection.

Logical and Arithmetic Propagation

pd.NA propagates through logical, arithmetic, and comparison operations with well-defined semantics. For example, True & pd.NA returns pd.NA because the result depends on the unknown value.

np.nan propagates in arithmetic (1 + np.nan → np.nan) but raises TypeError in logical operations on boolean arrays because np.nan is not a boolean value.

Detection with pd.isna() and pd.notna()

The utility functions in pandas/core/dtypes/missing.py handle both sentinels:


# Both are detected as missing

print(pd.isna(pd.NA))   # True

print(pd.isna(np.nan))  # True

# Works on Series with mixed markers

s = pd.Series([1, pd.NA, np.nan], dtype="object")
print(pd.isna(s))  # [False, True, True]

Practical Code Examples

Creating Series with Different Missing Value Types

import pandas as pd
import numpy as np

# Nullable integer dtype uses pd.NA

s_int = pd.Series([1, pd.NA, 3], dtype="Int64")
print(s_int)

# 0       1

# 1    <NA>

# 2       3

# Standard float dtype uses np.nan

s_float = pd.Series([1.0, np.nan, 3.0])
print(s_float)

# 0    1.0

# 1    NaN

# 2    3.0

# Nullable boolean dtype

s_bool = pd.Series([True, pd.NA, False], dtype="boolean")
print(s_bool)

# 0     True

# 1     <NA>

# 2    False

Logical Operations and Propagation


# pd.NA propagates through logical AND

s_bool = pd.Series([True, False, True], dtype="boolean")
result = s_bool & pd.NA
print(result)

# 0    <NA>

# 1    False

# 2    <NA>

# Reduction operations respect NA

print(s_bool.sum())              # 2 (skipna=True by default)

print(s_bool.sum(skipna=False))  # <NA>

# Creating object dtype Series by mixing markers

mixed = pd.Series([1, pd.NA, np.nan], dtype="object")
print(mixed.dtype)  # object

# Both detected as missing

print(pd.isna(mixed))

# 0    False

# 1     True

# 2     True

When to Use pd.NA vs np.nan

Choose pd.NA when:

  • Working with nullable extension dtypes (Int64, boolean, string, Float64)
  • You need consistent propagation through logical operations (&, |, ^)
  • Type preservation is critical (avoiding object dtype)
  • You want unified missing value semantics across integer, boolean, and string data

Choose np.nan when:

  • Working with pure NumPy arrays or float-only pandas Series
  • Memory efficiency is paramount and you only need float support
  • You require IEEE-754 compliant floating-point behavior
  • Backward compatibility with legacy code that expects float nan behavior

Summary

  • pd.NA is a dtype-agnostic singleton (NAType) implemented in pandas/_libs/missing.c designed for nullable extension dtypes, propagating consistently through logical and arithmetic operations.

  • np.nan is a float64 IEEE-754 sentinel from NumPy limited to float dtypes, following standard floating-point rules where it never equals itself.

  • Detection: Use pd.isna() and pd.notna() from pandas/core/dtypes/missing.py to identify both markers reliably.

  • Performance: pd.NA enables vectorized operations on nullable integers and booleans without object dtype overhead, while np.nan offers raw speed for float-only workflows.

Frequently Asked Questions

Can I mix pd.NA and np.nan in the same Series?

Yes, but doing so forces the Series to use object dtype, which eliminates performance benefits and type safety. When mixing occurs, pd.isna() correctly identifies both as missing values, but arithmetic and logical operations lose vectorization. For optimal performance, stick to one sentinel type per Series and use nullable extension dtypes with pd.NA.

Why does np.nan == np.nan return False?

This behavior follows the IEEE-754 floating-point standard, which specifies that NaN (Not a Number) is not equal to any value, including itself. This design prevents false positives in numerical computations where undefined results should not appear equal. To test for np.nan, always use np.isnan() or pd.isna() rather than equality operators.

Does pd.NA work with NumPy arrays?

pd.NA does not integrate natively with NumPy arrays because NumPy lacks native support for nullable integer or boolean dtypes. When you attempt to place pd.NA in a standard NumPy array, NumPy typically raises an error or forces object dtype conversion. Use pd.NA specifically with pandas Series and DataFrames that employ nullable extension dtypes (Int64, boolean, etc.).

Which is better for performance: pd.NA or np.nan?

For float-only data, np.nan offers slightly better raw performance because it avoids the extension array dispatch overhead. However, for integer, boolean, or string data, pd.NA is significantly faster because it allows vectorized operations through nullable extension dtypes without converting to slow object dtype. The pd.NA approach also reduces memory usage for non-float data by maintaining compact native buffers with separate boolean masks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →