Python has two distinct ways to represent missing or absent values: NaN (Not a Number) for numerical contexts and None for general-purpose absence. Understanding their differences is essential for data handling and avoiding subtle bugs.
Core Differences
| Feature | NaN | None |
|---|---|---|
| Type | float |
NoneType |
| Purpose | Missing numeric data | Absence of any value |
| Equality | nan != nan (always False) |
None == None (True) |
| Math operations | Propagates (nan + 1 = nan) |
Raises TypeError |
| Check method | math.isnan(x) |
x is None |
| Boolean value | True (truthy) |
False (falsy) |
Working with None
None is Python's built-in singleton representing the absence of a value. It's used for optional parameters, uninitialized variables, and functions without explicit returns.
x = None
print(type(x)) # <class 'NoneType'>
print(bool(x)) # False
print(x is None) # True
print(x == None) # True (but 'is' preferred)Checking for None
Always use the identity operator is rather than equality ==:
value = None
if value is None:
print("No value provided")
if value is not None:
print(f"Value exists: {value}")None in Functions
def find_user(user_id):
users = {1: "Alice", 2: "Bob"}
return users.get(user_id) # Returns None if not found
result = find_user(999)
if result is None:
print("User not found")Working with NaN
NaN represents undefined or unrepresentable numerical results. It's part of the IEEE 754 floating-point standard and behaves uniquely in comparisons.
import math
nan1 = float('nan')
nan2 = math.nan
print(type(nan1)) # <class 'float'>
print(type(nan2)) # <class 'float'>The Unusual Equality Behavior
NaN is the only value in Python that is not equal to itself:
import math
val = math.nan
print(val == val) # False
print(val == float('nan')) # False
print(val != val) # TrueOutput:
False
False
TrueChecking for NaN
Because nan != nan, you must use dedicated functions:
import math
val = float('nan')
if val == float('nan'):
print("This never executes")
if math.isnan(val):
print("Value is NaN")Output:
Value is NaNNaN Propagation in Math
NaN propagates through calculations, contaminating results:
import math
nan = math.nan
print(nan + 100) # nan
print(nan * 0) # nan
print(nan / nan) # nan
print(max(1, nan)) # 1None vs NaN in Practice
Type Behavior
import math
try:
result = None + 1
except TypeError as e:
print(f"None error: {e}") # unsupported operand type(s)
result = math.nan + 1
print(f"NaN result: {result}") # nanBoolean Context
import math
if not None:
print("None is falsy") # Prints
if math.nan:
print("NaN is truthy") # PrintsOutput:
None is falsy
NaN is truthyHandling Both in Pandas
Pandas treats None and NaN similarly in DataFrames, converting None to NaN in numeric columns:
import pandas as pd
import numpy as np
df = pd.DataFrame({
'numbers': [1.0, None, float('nan'), 4.0],
'strings': ['a', None, 'c', None]
})
print(df)
print(df.isna())Output:
numbers strings
0 1.0 a
1 NaN None
2 NaN c
3 4.0 None
numbers strings
0 False False
1 True True
2 True False
3 False TrueChecking and Filling Missing Values
import pandas as pd
import numpy as np
df = pd.DataFrame({
'value': [1, None, np.nan, 4],
'category': ['A', None, 'B', 'C']
})
print(df.isna().sum())
df_filled = df.fillna({'value': 0, 'category': 'Unknown'})
print(df_filled)Output:
value 2
category 1
dtype: int64
value category
0 1.0 A
1 0.0 Unknown
2 0.0 B
3 4.0 CDistinguishing None from NaN in Pandas
When you need to differentiate between them:
import pandas as pd
import numpy as np
data = pd.Series([1, None, np.nan], dtype=object)
def check_missing(val):
if val is None:
return 'None'
try:
if np.isnan(val):
return 'NaN'
except (TypeError, ValueError):
pass
return 'Value'
print(data.apply(check_missing))Output:
0 Value
1 None
2 NaN
dtype: objectComprehensive Missing Value Check
When you need to handle both types robustly:
import math
def is_missing(value):
"""Check if value is None or NaN."""
if value is None:
return True
try:
return math.isnan(value)
except (TypeError, ValueError):
return False
test_values = [None, float('nan'), 0, '', [], 42, 'hello']
for val in test_values:
print(f"{str(val):10} -> missing: {is_missing(val)}")Output:
None -> missing: True
nan -> missing: True
0 -> missing: False
-> missing: False
[] -> missing: False
42 -> missing: False
hello -> missing: FalseWhen to Use Each
| Scenario | Recommended Type | Reason |
|---|---|---|
| Function returns no result | None |
Pythonic convention |
| Optional parameter default | None |
Standard practice |
| Missing sensor reading | NaN |
Preserves float dtype |
| Database NULL in numeric column | NaN |
Allows vectorized operations |
| Database NULL in object column | None |
Natural representation |
| Uninitialized variable | None |
Explicit absence |
| Mathematical undefined result | NaN |
IEEE 754 standard |
Summary
- Use
Nonefor general absence of values: function returns, optional parameters, and non-numeric missing data. - Use
NaNfor missing numerical data where you need to maintain float dtype and perform vectorized operations. - In Pandas, both are treated as missing values and detected by
isna(), but understanding their distinct behaviors prevents subtle bugs in your data processing pipelines.