From 285ddd3234497a29113fec476201419dd6f9c9d4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 10 Nov 2025 18:39:01 +0200 Subject: [PATCH] gh-85588: Restore the __ne__() method in Set and Mapping Set it to object.__ne__. This guarantees that the != operator is consistent with the == operator, even if the __ne__ method was defined in other parent class. --- Lib/_collections_abc.py | 4 ++++ Lib/test/test_collections.py | 22 ++++++++++++++++--- ...5-11-10-18-38-58.gh-issue-85588.562vvi.rst | 5 +++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-11-10-18-38-58.gh-issue-85588.562vvi.rst diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 60b471317ce97ca..da16c72efccf6c4 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -594,6 +594,8 @@ def __eq__(self, other): return NotImplemented return len(self) == len(other) and self.__le__(other) + __ne__ = object.__ne__ + @classmethod def _from_iterable(cls, it): '''Construct an instance of the class from any iterable input. @@ -821,6 +823,8 @@ def __eq__(self, other): return NotImplemented return dict(self.items()) == dict(other.items()) + __ne__ = object.__ne__ + __reversed__ = None Mapping.register(mappingproxy) diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 225952392528143..2a2dae493066573 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -751,7 +751,8 @@ def validate_isinstance(self, abc, name): self.assertNotIsInstance(C(), abc) self.assertNotIsSubclass(C, abc) - def validate_comparison(self, instance): + def validate_comparison(self, klass): + instance = klass() ops = ['lt', 'gt', 'le', 'ge', 'ne', 'or', 'and', 'xor', 'sub'] operators = {} for op in ops: @@ -782,6 +783,21 @@ def __eq__(self, other): self.assertTrue(other.right_side,'Right side not called for %s.%s' % (type(instance), name)) + # gh-85588: Inherited __ne__ should be overriddent together with __eq__ + # in Set and Mapping. + class Mixin: + def __eq__(self, other): + raise AssertionError('should not be called') + __ne__ = __eq__ + class C(klass, Mixin): + pass + instance = C() + other = object() + self.assertIs(instance == other, False) + self.assertIs(instance != other, True) + self.assertIs(instance.__eq__(other), NotImplemented) + self.assertIs(instance.__ne__(other), NotImplemented) + def _test_gen(): yield @@ -1430,7 +1446,7 @@ def __len__(self): return 0 def __iter__(self): return iter([]) - self.validate_comparison(MySet()) + self.validate_comparison(MySet) def test_hash_Set(self): class OneTwoThreeSet(Set): @@ -1852,7 +1868,7 @@ def __getitem__(self, i): raise IndexError def __iter__(self): return iter(()) - self.validate_comparison(MyMapping()) + self.validate_comparison(MyMapping) self.assertRaises(TypeError, reversed, MyMapping()) def test_MutableMapping(self): diff --git a/Misc/NEWS.d/next/Library/2025-11-10-18-38-58.gh-issue-85588.562vvi.rst b/Misc/NEWS.d/next/Library/2025-11-10-18-38-58.gh-issue-85588.562vvi.rst new file mode 100644 index 000000000000000..49a2f4a6ed3ffc7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-10-18-38-58.gh-issue-85588.562vvi.rst @@ -0,0 +1,5 @@ +Restore the :meth:`!__ne__` method (identical to :meth:`object.__ne__`) in +:class:`collections.abc.Set` and :class:`collections.abc.Mapping` classes. +This guarantees that the ``!=`` operator is consistent with the ``==`` +operator, even if the :meth:`!__ne__` method was defined in other parent +class.