3

Given the following class, how should I annotate __eq__?

class Foo(object):
    def __init__(self, bar):
        self.bar = bar

    def __eq__(self, other):
        return Foo.bar == other.bar

Using __eq__(self, other: Foo) -> bool makes mypy error with:

Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object"

and using __eq__(self, other: object) -> bool makes mypy error with:

"object" has no attribute "bar"

What should I do?

Dustin Ingram
  • 20,502
  • 7
  • 59
  • 82

2 Answers2

3

It is recommended that __eq__ works with arbitrary objects. In this example, you would need to guard that other is a member of the Foo class first:

def __eq__(self, other: object) -> bool:
    if not isinstance(other, Foo):
        return NotImplemented
    return Foo.bar == other.bar
Dustin Ingram
  • 20,502
  • 7
  • 59
  • 82
  • 1
    @schwobaseggl It's preferable to `return NotImplemented` rather than `raise NotImplementedError`, see https://stackoverflow.com/questions/878943/why-return-notimplemented-instead-of-raising-notimplementederror – Dustin Ingram Sep 12 '19 at 11:56
1

object.__eq__() must handle any type of object, not just your own class. Return NotImplemented for types you don't want to handle, so Python can ask the right-hand-side operand if it wants to handle equality testing.

Annotate the other argument as object as per the Python typeshed guidelines:

def __eq__(self, other: object) -> bool:
    if not isinstance(other, Foo):
        return NotImplemented
    return Foo.bar == other.bar

Setting other to object essentially tells typecheckers that anything that inherits from object (which in Python is everything) is acceptable. The isinstance(other, Foo) test tells a type checker that any object that makes it to the last line of the function is not only an instance of object, but specifically an instance of Foo or a further subclass.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 2
    I disagree with using Any here. While both Any and object mean "anything can be passed in here", Any has the additional downside of meaning "also, please subvert the type system". So, using it is counter-productive if your goal is to ensure your code is statically typed -- it's really only meant to be an escape hatch when you're unable to adequately express something using types. The [Typeshed contribution guidelines](https://github.com/python/typeshed/blob/master/CONTRIBUTING.md#conventions) goes into a little more detail about this. – Michael0x2a Sep 12 '19 at 20:36
  • `object.__eq__` is canonically defined to [accept object](https://github.com/python/typeshed/blob/master/stdlib/2and3/builtins.pyi#L47) in typeshed for this very reason. (So to be consistent/as per Liskov, it's probably best if all subclasses do the same.) It is however definitely true that type checkers are not obligated to understand isinstance checks. But all of them seem to, so I'm not sure if that's really an issue in practice. – Michael0x2a Sep 12 '19 at 20:39
  • 1
    @Michael0x2a: ick, indeed, that typeshed reference is pretty definitive. I'll have to hunt out the [incorrect type annotations in typeshed](https://github.com/python/typeshed/blob/efb67946f88494e5e9a273d59a3100a56514b15f/stdlib/3/email/charset.pyi#L24) to avoid making the same mistake again. – Martijn Pieters Sep 12 '19 at 20:47