18

Edit: I notice people commenting about how the type hint should not be used with __eq__, and granted, it shouldn't. But that's not the point of my question. My question is why can't the class be used as type hint in the method parameters, but can be used in the method itself?


Python type hinting has proven very useful for me when working with PyCharm. However, I've come across behaviour I find strange, when trying to use a class' own type in its methods.

For example:

class Foo:

    def __init__(self, id):
        self.id = id
        pass

    def __eq__(self, other):
        return self.id == other.id

Here, when typing other., the property id is not offered automatically. I was hoping to solve it by defining __eq__ as follows:

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

However, this gives NameError: name 'Foo' is not defined. But when I use the type within the method, id is offered after writing other.:

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

My question is, why is it not possible to use the class' own type for type hinting the parameters, while it is possible within the method?

R. dV
  • 416
  • 1
  • 3
  • 15
  • That's the wrong type hint for `__eq__` anyway. `__eq__` is supposed to take arbitrary other objects and return `NotImplemented` for types it can't compare. – user2357112 Aug 20 '20 at 10:54
  • While I agree with you, this is just a simple example to show. For any method of class ```Foo```, it's not possible to use ```Foo``` as type hint. – R. dV Aug 20 '20 at 10:56

2 Answers2

32

The name Foo doesn't yet exist, so you need to use 'Foo' instead. (mypy and other type checkers should recognize this as a forward reference.)

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

Alternately, you can use

from __future__ import annotations

which prevents evaluation of all annotations and simply stores them as strings for later reference. (This will be the default in Python 3.10.)

Finally, as also pointed out in the comments, __eq__ should not be hinted this way in the first place. The second argument should be an arbitrary object; you'll return NotImplemented if you don't know how to compare your instance to it. (Who knows, maybe it knows how to compare itself to your instance. If Foo.__eq__(Foo(), Bar()) returns NotImplemented, then Python will try Bar.__eq__(Bar(), Foo()).)

from typing import Any


def __eq__(self, other: Any) -> bool:
    if isinstance(other, Foo):
        return self.id == other.id
    return NotImplemented

or using duck-typing,

def __eq__(self, other: Any) -> bool:
    # Compare to anything with an `id` attribute
    try:
        return self.id == other.id
    except AttributeError:
        return NotImplemented

In either case, the Any hint is optional.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    "The name Foo doesn't yet exist, so you need to use 'Foo' instead". This is an answer in the direction I'm looking for. You say it does not exist yet, but it can be used in the method itself. My question is why does Python think Foo does not yet exists, as the method is per definition inside the Foo class? – R. dV Aug 20 '20 at 12:03
  • 1
    @R.dV as far as I understand it, Python only evaluates the contents of a function/method once it is called. The function definition on the other hand needs to be evaluated before actually calling it. So you define a class, but before the class is defined its methods must be known, so the definitions need to be evaluated, but in one method definition you tell Python, that it takes a class that is not yet defined, so it throws up on you. Not sure if that's 100% correct, but I hope that could clear things up – Fynn Sep 26 '22 at 10:58
12

As of python 3.11 it's now possible to use Self to type hint the current class.

For example, this is now valid python:

from typing import Self

class Foo:
   def return_self(self) -> Self:
      ...
      return self

Here is a link to the docs

And is a link to another answer on stack overflow

user1239299
  • 665
  • 8
  • 26