# Make type hints for \`\_\_eq\_\_\` of primitives less strict

**URL:** <https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240>\
**Category:** Typing\
**Tags:** typing\
**Created:** [September 20, 2023, 7:15pm UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240 "2023-09-20T19:15:23Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![mick](https://avatars.discourse-cdn.com/v4/letter/m/f04885/32.png) [@mick](https://discuss.python.org/u/mick)\
**Post date:** [September 20, 2023, 7:15pm UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/1 "2023-09-20T19:15:24Z")

</div>

Reviving an old topic from [GH 8217](https://github.com/python/typeshed/issues/8217)

Basically many packages re-define ` __eq__ ` to return custom classes, including numpy and pandas.  
This conflicts with the typeshed definition that should return bool.  
But since the typeshed definition accepts `object` types any operation like

```python
[1, 2, 3] == np.array([1, 2, 3])

```

will be inferred to bool when in actuality it is a `np.ndarray`.  
Additionally, there is no way to overwrite this using ignores.

---

<div class="post-metadata">

**Author:** ![srittau](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/srittau/32/237_2.png) [@srittau](https://discuss.python.org/u/srittau)\
**Post date:** [September 20, 2023, 9:58pm UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/2 "2023-09-20T21:58:00Z")

</div>

Per the issue, we should try returning `NotImplemented` in cases where the comparison is not explicitly supported. For example,

```python
class list(...):
    @overload
    def __eq__ (self, other: listOrWhatever) -> bool: ...
    @overload
    def __eq__ (self, other: object) -> NotImplemented: ...

```

An exploratory PR to just the effect of such a change would be very welcome. We could try to express `object. __eq__ ()` like this:

```python
class object:
    @overload
    def __eq__ (self, other: Self) -> Literal[True]: ...
    @overload
    def __eq__ (self, other: object) -> NotImplemented: ...

```

But that would mean `# type: ignore` in all sub classes that implement ` __eq__ `. I think it’s safer to use:

```python
class object:
    @overload
    def __eq__ (self, other: Self) -> Literal[True]: ...
    @overload
    def __eq__ (self, other: object) -> NotImplemented | bool: ...

```

---

<div class="post-metadata">

**Author:** ![davidfstr](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/davidfstr/32/6944_2.png) [@davidfstr](https://discuss.python.org/u/davidfstr)\
**Post date:** [September 20, 2023, 10:44pm UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/3 "2023-09-20T22:44:27Z")

</div>

> [@mick](#):
>
> `[1, 2, 3] == np.array([1, 2, 3])`

Could you swap the operand order by writing `np.array([1, 2, 3]) == [1, 2, 3]`? I speculate then you could define an ` __eq__ ` on np.array that returns something other than bool.

---

<div class="post-metadata">

**Author:** ![superbobry](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/superbobry/32/7013_2.png) [@superbobry](https://discuss.python.org/u/superbobry)\
**Post date:** [September 21, 2023, 8:41am UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/4 "2023-09-21T08:41:43Z")

</div>

That will be a an invalid override of [`object. __eq__ `](https://github.com/python/typeshed/blob/53422f8918debdaf60e1605d0e2e3ce77a810afd/stdlib/builtins.pyi#L106) which currently returns `bool`.

---

<div class="post-metadata">

**Author:** ![mick](https://avatars.discourse-cdn.com/v4/letter/m/f04885/32.png) [@mick](https://discuss.python.org/u/mick)\
**Post date:** [September 21, 2023, 9:15am UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/5 "2023-09-21T09:15:11Z")

</div>

That’s what numpy and pandas are already doing (and ignoring the invalid override).

And just to cite your documentation [https://docs.python.org/3/library/operator.html:](https://docs.python.org/3/library/operator.html:)

> Note that these functions can return any value, which may or may not be interpretable as a Boolean value.

---

<div class="post-metadata">

**Author:** ![AlexWaygood](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/alexwaygood/32/4722_2.png) [@AlexWaygood](https://discuss.python.org/u/AlexWaygood)\
**Post date:** [September 21, 2023, 11:30am UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/6 "2023-09-21T11:30:55Z")

</div>

> [@srittau](#):
>
> I think it’s safer to use:
> 
> ```auto
> class object:
> @overload
> def __eq__ (self, other: Self) -> Literal[True]: ...
> @overload
> def __eq__ (self, other: object) -> NotImplemented | bool: ...
> 
> ```

I don’t think the first overload is accurate. With these overloads, a type checker would pick the first overload to infer the type here, leading to an inferred type of `Literal[True]`. But at runtime, `z` is `False`.

```py
x = object()
y = object()
z = x == y
reveal_type(z)

```

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [September 22, 2023, 12:01am UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/7 "2023-09-22T00:01:19Z")

</div>

changing the base typing to:

```py
class HasBool(Protocol):
    def __bool__ (self) -> bool:
        ...

class object:
    def __eq__ (self, other: object) -> HasBool:
        ...

```

Would remain accurate to how the implicit calls to bool may happen, while allowing subclasses so long as the non-bool object they return on `==` provides an ` __eq__ ` implementation, even if that implementation only ever errors to inform people they are missing something intended for use in alternative ways (pandas, sqla, etc)

---

<div class="post-metadata">

**Author:** ![mick](https://avatars.discourse-cdn.com/v4/letter/m/f04885/32.png) [@mick](https://discuss.python.org/u/mick)\
**Post date:** [September 22, 2023, 5:10am UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/8 "2023-09-22T05:10:27Z")

</div>

So there are two issues here, one about the return type and one about the “other” type.

The return type problem is less severe because one can type ignore custom solutions.

But it would be nice if list etc. would not lie about which types they can operate on.  
I think the confusion here is that while it is true that it can accept any argument at runtime, from a typing perspective it would be enough to say e.g. `def **eq** (self, other: Self) → bool and type checkers will know what to do. I don’t think the extra overloads are necessary.

---

<div class="post-metadata">

**Author:** ![mmohaveri](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mmohaveri/32/14833_2.png) [@mmohaveri](https://discuss.python.org/u/mmohaveri)\
**Post date:** [September 23, 2023, 2:16pm UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/9 "2023-09-23T14:16:18Z")

</div>

I don’t agree with this implementation. This implementation still assumes that ` __eq__ ` to return a truthy value, which in return results in wrong type inference in the type checking phase.

Just to be clear about what the original issue is I’m gonna paste it here:

> As can be seen in [builtins.pyi](https://github.com/python/typeshed/blob/master/stdlib/builtins.pyi), all primitives are defining their ` __eq__ ` and ` __ne__ ` functions in the following manner:

```python
   def __eq__ (self, __x: object) -> bool: ...
   def __ne__ (self, __x: object) -> bool: ...

```

> This will cause incorrect type errors in cases where the second operand has overloaded ` __eq__ ` or ` __ne__ ` in a way that it does not return `bool`. Consider the following example:

```python
T = TypeVar("T", float, bool)

class Matrix(Generic[T]):
  def __init__ (self, data: List[List[T]]):
    self.data = data

  def __eq__ (self, other: "Matrix[T] | float | bool") -> Matrix[bool]:
    if not isinstance(other, Matrix):
      return Matrix([[elem == other for elem in row] for row in self.data])
    return Matrix((elem == other_elem for elem, other_elem in zip(row1, row2)] for row1, row2 in zip(self.data, other.data)]

  def __ne__ (self, other: "Matrix[T] | float | bool") -> Matrix[bool]: ...

```

> Here we’re trying to create a class that mimics numpy’s `NDArray` in a very limited way.

> Now if I try to check the equality of a `Matrix` with a `float`, the return type will be inferred as `Matrix`, since LHS of the operation has a matching ` __eq__ ` method which returns matrix (and the returned type in runtime is `Matrix` as well)

> But if I try to check the equality of a `float` with a `Matrix`, the returned type will be inferred as `bool` because LHS of the operation has a compatible ` __eq__ ` (`def __eq__ (self, __x: object) -> bool`, bject and `Matrix` ar compatible) while in the runtime because float’s **eq** will return a ` NotImplemented`, interpreter will try to run the `__ne__` function of the RHS which returns a `Matrix`.

> This will create situations where the inferred type of a simple equality check operation does not match the runtime’s type, all because primitives stubs has been defined in a way that is not representative of their runtime behavior.

---

<div class="post-metadata">

**Author:** ![mmohaveri](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mmohaveri/32/14833_2.png) [@mmohaveri](https://discuss.python.org/u/mmohaveri)\
**Post date:** [September 23, 2023, 8:59pm UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/10 "2023-09-23T20:59:57Z")

</div>

As any class that returns a non-boolean in its ` __eq__ ` is effectively breaking the _Liskov substitution principle_, I don’t think we’ll be able to create a consistent type for ` __eq__ ` without forcing `object`’s subclasses to use a `# type: ignore` due to incompatible overwrite.

### Solution:

This is my suggestion regarding the ` __eq__ `'s type hint in order for the hint to correctly reflect CPython’s implementation of the equality check process:

For `object`:

```ptyhon
class object:
    def __eq__ (self, other: object) -> NotImplemented: ...

```

For other builtin classes\[1\], look at the `CPython`’s implementation of ` __eq__ ` to determine their appropriate type hints. For example, the `float`’s and `complex`’s ` __eq__ ` will be defined as:

```python
class float:
    def __eq__ (self, other: int | float) -> bool: ...

class complex:
    def __eq__ (self, other: int | float | complex) -> bool: ...

```

That’s because based on CPython’s code float’s ` __eq__ ` will return an UnimplementedError if the other operand is not an int or float, while complex’s ` __eq__ ` will return an UnimplementedError if the other operand is not an int, float, **or complex**. Take a look at `float_richcompare` and `complex_richcompare` functions in CPython’s source code.

### Rational

I understand that this will require almost all ` __eq__ ` overwrites to have a `# type: ignore` comment due to them being incompatible with the `object`’s one, but the reality of the matter is that the way that Python’s equality check works forces us to do so **if we want to have type hints that reflects the runtime’s behavior.**

Also I understand that defining the object’s ` __eq__ ` in a way that always returns a `NotImplemented` is controversial, but that’s how the CPython’s interpreter is working. It’s a little hard to explain my rational for this, so bare with me.

First, Keep in mind that when python encounters `==` it first calls the LHS’s ` __eq__ `, if it does not exist or returns a `NotImplemented`, then it calls ` __eq__ ` of the RHS. If it does not exist or returns a `NotImplemented` then it compare’s the object pointers (i.e: if the LHS and RHS are same object in the momory it returns True, otherwise False).

Now take a look at following code:

```python
class A:
   def __eq__ (self, other):
      print("A")
      return NotImplemented

class B:
   def __eq__ (self, other):
      print("B")
      return NotImplemented

class C:
   def __eq__ (self, other):
      print("C")
      return True

object() == object() # (1) Evaluates to False, as their distinct objects
A() == B() # (2) Prints 'A' then 'B' then evaluates to False, as their distinct objects
B() == A() # (3) Prints 'B' then 'A' then evaluates to False, as their distinct objects
object() == A() # (4) Prints 'A' then evaluates to False, as their distinct objects
A() == object() # (5) Prints 'A' then evaluates to False, as their distinct objects
object() == C() # (6) Prints 'C' then evaluates to True, as C's __eq__ always return True
C() == object() # (7) Prints 'C' then evaluates to True, as C's __eq__ always return True
A() == C() # (8) Prints 'A' then 'C' then evaluates to True, as C's __eq__ always return True
C() == A() # (9) Prints 'C' then evaluates to True, as C's __eq__ always return True

```

These results tells me that `object()` does not have an ` __eq__ ` implementation (look at `(4)` & `(5)`) and unless the _other_ value defines a valid ` __eq__ `, the equality check will fall back to the object pointer comparison (look at `(6)` & `(7)`).

Again, I know this proposal in controversial, but I think it’s the only way to type hint ` __eq__ ` in a way the lets type checkers infer the correct result of a comparison when comparing a builtin object with a custom object with a non-standard ` __eq__ ` overwrite.

* * *

1. The builtin classes that typeshed provides type hints for are: `int`, `float`, `complex`, `str`, `bytes`, `bytearray`, `memoryview`, `slice`, `tuple`, `list`, `dict`, `set`, `frozenset`, and `range`.

---

<div class="post-metadata">

**Author:** ![Azureblade3808](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/azureblade3808/32/15939_2.png) [@Azureblade3808](https://discuss.python.org/u/Azureblade3808)\
**Post date:** [November 21, 2023, 7:41am UTC](https://discuss.python.org/t/make-type-hints-for-eq-of-primitives-less-strict/34240/12 "2023-11-21T07:41:10Z")

</div>

Sorry about my last post. I’ve missed that it’s the argument type causes the need of `# type: ignore`.

Here’s my 2c –

For `object`, ` __eq__ ` is removed as if it doesn’t exist. For other classes, ` __eq__ ` is declared only for supported RHS types.

```python
class object:
    # No ` __eq__ ` method.
    ...

class float:
    def __eq__ (self, other: int | float) -> bool: ...

class complex:
    def __eq__ (self, other: int | float | complex) -> bool: ...

```

For expression like `a == b`, a type checker should first attempt evaluating `a. __eq__ (b)`, then `b. __eq__ (a)` if first attempt failed, and finally decide that the result should be `Literal[False]` if both previous attempts failed.
