This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Clarify the behavior of __eq__() returning NotImplemented
Type: enhancement Stage:
Components: Documentation Versions: Python 3.6
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: docs@python Nosy List: docs@python, martin.panter, max
Priority: normal Keywords:

Created on 2016-11-24 08:14 by max, last changed 2022-04-11 14:58 by admin.

Messages (4)
msg281616 - (view) Author: Max (max) * Date: 2016-11-24 08:14
Currently, there's no clear statement as to what exactly the fallback is in case `__eq__` returns `NotImplemented`.  It would be good to clarify the behavior of `NotImplemented`; at least for `__eq__`, but perhaps also other rich comparison methods. For example: "When `NotImplemented` is returned from a rich comparison method, the interpreter behaves as if the rich comparison method was not defined in the first place." See http://stackoverflow.com/questions/40780004/returning-notimplemented-from-eq for more discussion.
msg281625 - (view) Author: Martin Panter (martin.panter) * (Python committer) Date: 2016-11-24 09:46
The documentation of this that comes to mind is spread over <https://docs.python.org/3.6/reference/datamodel.html#object.__eq__> and <https://docs.python.org/3.6/reference/expressions.html#value-comparisons>.

My understanding of how it works in general is that NotImplemented is just a simple object like None. It doesn’t have any special behaviour on its own. Similarly, if you call a special method like __eq__() manually, there is no fallback, you just get the NotImplemented object. The interpreter calls special methods like __eq__() when evaluating expressions, but the specific behaviour and how NotImplemented is interpreted varies subtly.

To evaluate a == b, if b’s class is a strict subclass of a’s class, b.__eq__(a) is tried first, otherwise a.__eq__(b) is tried first. If that call returns NotImplemented, the first fallback is to try the call. Failing that, the final fallback is the default behaviour for object() instances.

To evaluate a != b, there is another set of fallbacks in the chain, which is to try “not a.__eq__(b)”. So for __eq__(), the fallback depends on the class hierarchy, on whether a reversed call has already been made, and on whether you are evaluating == or !=.

The documentation for comparison operations was cleaned up a while ago (Issue 12067). But there is probably more room for improvement. I think the best thing would be to document the above sort of behaviour as being directly associated with operations like as == and !=, and only indirectly associated with the NotImplemented object and the __eq__() method.
msg281628 - (view) Author: Max (max) * Date: 2016-11-24 10:38
Martin - what you suggest is precisely what I had in mind (but didn't phrase it as well):

> to document the above sort of behaviour as being directly associated with operations like as == and !=, and only indirectly associated with the NotImplemented object and the __eq__() method

Also a minor typo: you meant "If that call returns NotImplemented, the first fallback is to try the *reverse* call."
msg281714 - (view) Author: Martin Panter (martin.panter) * (Python committer) Date: 2016-11-25 13:15
Correct, I meant to say the first fallback is the other call.

BTW your suggested text might hold for __eq__(), but for __ne__(), returning NotImplemented seems to bypass the “not a.__eq__(b)” fallback.
History
Date User Action Args
2022-04-11 14:58:40adminsetgithub: 72971
2017-01-22 04:54:48martin.panterlinkissue15997 dependencies
2017-01-22 04:45:40martin.pantersettitle: Clarify the behavior of NotImplemented -> Clarify the behavior of __eq__() returning NotImplemented
2016-11-25 13:15:05martin.pantersetmessages: + msg281714
2016-11-24 10:38:11maxsetmessages: + msg281628
2016-11-24 09:46:24martin.pantersetnosy: + martin.panter
messages: + msg281625
2016-11-24 08:14:24maxcreate