2025, Nov 01 21:00

How to Test NaN in Python: Reliable sequence comparisons in unittest using math.isnan and pandas

Learn why NaN breaks equality in Python (IEEE-754) and how to test sequences from pandas reliably using unittest, a NaN matcher, and math.isnan. With examples.

Testing code that produces NaN often turns into a trap: NaN represents “not a number”, and by IEEE-754 rules any comparison with NaN is false, including NaN == NaN. When your function legitimately returns NaN values, a straightforward equality check on sequences fails even if both sides visually look the same. Here’s how to make such tests robust without sacrificing useful unittest diagnostics.

Reproducing the failure

The scenario: a rolling mean with a window of three elements. The first two positions can’t be computed and are NaN, so the expected list contains NaNs at the front. The test below compares two lists directly and fails, even though the numbers look identical.

from math import nan
import unittest
import pandas as pd
class DataBin:
    def __init__(self, seed: list) -> None:
        self.seed = seed
        self.values = []
    def compute_window_mean(self):
        self.values = pd.Series(self.seed).rolling(3).mean().to_list()
class RollingMeanSpec(unittest.TestCase):
    def setUp(self):
        self.bin = DataBin(seed=[10, 20, 30, 40])
    def test_three_point_window_on_list(self):
        expected = [nan, nan, 20.0, 30.0]
        self.bin.compute_window_mean()
        self.assertSequenceEqual(self.bin.values, expected)
unittest.main()

The assertion error shows the mismatch at the very first element:

AssertionError: Sequences differ: [nan, nan, 20.0, 30.0] != [nan, nan, 20.0, 30.0]
First differing element 0:
nan
nan
  [nan, nan, 20.0, 30.0]

What actually breaks

The values all represent NaN, but they aren’t identical objects and they don’t compare equal. A minimal example isolates the root cause:

import math
import unittest
class CheckNaNEquality(unittest.TestCase):
    def test_single_nan_item(self):
        self.assertEqual([float("nan")], [math.nan])
if __name__ == "__main__":
    unittest.main()
AssertionError: Lists differ: [nan] != [nan]
First differing element 0:
nan
nan
  [nan]

The problem in the rolling-mean case comes from pd.Series.to_list returning new float("nan") values, not math.nan. Both are NaN, but neither equals the other. A direct list equality, therefore, fails.

Note: you can generally call self.assertEqual and have it dispatch to the appropriate underlying implementation.

A precise fix with a matcher

The clean way to compare “this position must be NaN” is to match by the property “is NaN”, not by identity or strict equality. That’s easy to encode in a tiny helper used only in tests.

import math
import typing as tp
class NaNToken:
    def __eq__(self, other: tp.Any) -> bool:
        return math.isnan(other)
    def __repr__(self) -> str:
        return "nan"

With that in place, the expected list can declare NaN positions explicitly and keep full per-element diffs on failure:

import unittest
import pandas as pd
class DataBin:
    def __init__(self, seed: list) -> None:
        self.seed = seed
        self.values = []
    def compute_window_mean(self):
        self.values = pd.Series(self.seed).rolling(3).mean().to_list()
class RollingMeanSpec(unittest.TestCase):
    def setUp(self):
        self.bin = DataBin(seed=[10, 20, 30, 40])
    def test_three_point_window_on_list(self):
        expected = [NaNToken(), NaNToken(), 20.0, 30.0]
        self.bin.compute_window_mean()
        self.assertEqual(self.bin.values, expected)
if __name__ == "__main__":
    unittest.main()

If a non-NaN value diverges, unittest still reports a precise diff rather than collapsing everything into a single boolean:

AssertionError: Lists differ: [nan, nan, 20.0, 30.0] != [nan, nan, 20.0, 35.0]
First differing element 3:
30.0
35.0
- [nan, nan, 20.0, 30.0]
?                   ^
+ [nan, nan, 20.0, 35.0]
?    

Contrast that with an approach that flattens the whole check to a single boolean, which discards all diagnostics:

AssertionError: False is not true

Why it’s worth knowing

When a function legitimately emits NaN, a naïve equality check encourages brittle tests or opaque workarounds. Matching on the property “is NaN” preserves intent, prevents false negatives caused by different NaN instances, and keeps unittest’s detailed element-by-element reporting intact. It also allows you to keep your expected structures readable and close to the domain result you’re checking.

Takeaways

If a test compares sequences that may contain NaN, compare by the predicate “is NaN” instead of equality. pd.Series.to_list returns fresh float("nan") values, which will not equal math.nan or each other, so direct list equality breaks. A small matcher that uses math.isnan fixes the comparison while preserving rich diffs, and you can use self.assertEqual to let unittest choose the appropriate comparison logic.

The article is based on a question from StackOverflow by Charizard_knows_to_code and an answer by jonrsharpe.