Repository: biopython/biopython

Issue:
Remove type annotations from strictly_equals methods in Bio.PDB
For some reason, all checks passed on #4593 but after merging mypy started complaining. The issue seems to be overloading `strictly_equals` in Entity subclasses (and in Atom.py) violates some typing rule. I cannot figure out a way to handle this, so I simply removed the type annotation from these methods..



Discussion:
This happens because types of function/method arguments are contravariant (the Python typing specification, especially the [Variance of generic types section on `● Callable`](https://typing.readthedocs.io/en/latest/source/generics.html#variance-of-generic-types), is highly relevant).

The easiest fix is to use [`typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) like this (demo: [mypy Playground](https://mypy-play.net/?mypy=latest&python=3.12&flags=strict&gist=09c58a6df3a406bc18165825a62a0b69))

```python
class Entity(Generic[_Parent, _Child]):
    def strictly_equals(self, other: Self, compare_coordinates: bool = False) -> bool: ...

class Residue(Entity["Chain", "Atom"]):
    def strictly_equals(self, other: Self, compare_coordinates: bool = False) -> bool: ...

class Atom:
    def strictly_equals(self, other: Self, compare_coordinates: bool = False) -> bool: ...
```

However, since this was only introduced in Python 3.11, and Biopython probably (?) doesn't want `typing_extensions` to be a runtime dependency, you can either do this

```python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # This is fine regardless whether it is actually installed, because all type checkers rely on their own `typing_extensions` `.pyi` stubs, not any runtime installation.
    # ---
    # mypy: https://github.com/python/mypy/blob/master/mypy/typeshed/stdlib/typing_extensions.pyi
    # pyright: https://github.com/microsoft/pyright/blob/main/packages/pyright-internal/typeshed-fallback/stdlib/typing_extensions.pyi
    # pytype: https://github.com/google/pytype (`[typeshed @ <commit hash>]`)
    # pyre: https://github.com/facebook/pyre-check/blob/main/stubs/typeshed/typeshed/stdlib/typing_extensions.pyi
    from typing_extensions import Self

class Entity(Generic[_Parent, _Child]):
    def strictly_equals(self, other: "Self", compare_coordinates: bool = False) -> bool: ...
```

Or this ([mypy Playground](https://mypy-play.net/?mypy=latest&python=3.12&flags=strict&gist=bcab7f7c42bdc888058bfa4fceb66b0b)):

```python
_EntityT = TypeVar("_EntityT", bound="Entity")
_ResidueT = TypeVar("_ResidueT", bound="Residue")
_AtomT = TypeVar("_AtomT", bound="Atom")

class Entity(Generic[_Parent, _Child]):
    def strictly_equals(self: _EntityT, other: _EntityT, compare_coordinates: bool = False) -> bool: ...

class Residue(Entity["Chain", "Atom"]):
    def strictly_equals(self: _ResidueT, other: _ResidueT, compare_coordinates: bool = False) -> bool: ...

class Atom:
    def strictly_equals(self: _AtomT, other: _AtomT, compare_coordinates: bool = False) -> bool: ...
```
We're a long way from requiring Python 3.11, so that rules out the first option as you say.

I don't know enough to judge the merits of the ``import Self`` versus ``TypeVar`` approaches you suggest.
The `TypeVar` approach is more transparent and is idiomatic Python.

While I can't foresee any issues, I think the `typing_extensions` import under `TYPE_CHECKING` is highly unconventional.
@bzoracler do you fancy making that into a pull request? We can merge this now as a stop gap...

Resolving patch:
diff --git a/Bio/PDB/Atom.py b/Bio/PDB/Atom.py
--- a/Bio/PDB/Atom.py
+++ b/Bio/PDB/Atom.py
@@ -9,7 +9,7 @@
 
 import copy
 import sys
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING, TypeVar
 import warnings
 
 import numpy as np
@@ -23,6 +23,8 @@
 if TYPE_CHECKING:
     from Bio.PDB.Residue import Residue
 
+_AtomT = TypeVar("_AtomT", bound="Atom")
+
 
 class Atom:
     """Define Atom class.
@@ -267,7 +269,9 @@ def __sub__(self, other):
         diff = self.coord - other.coord
         return np.sqrt(np.dot(diff, diff))
 
-    def strictly_equals(self, other: "Atom", compare_coordinates: bool = False) -> bool:
+    def strictly_equals(
+        self: _AtomT, other: _AtomT, compare_coordinates: bool = False
+    ) -> bool:
         """Compare this atom to the other atom using a strict definition of equality.
 
         Indicates whether the atoms have the same name, B factor, occupancy,
diff --git a/Bio/PDB/Entity.py b/Bio/PDB/Entity.py
--- a/Bio/PDB/Entity.py
+++ b/Bio/PDB/Entity.py
@@ -25,6 +25,7 @@
 
 _Child = TypeVar("_Child", bound=Union["Entity", "Atom"])
 _Parent = TypeVar("_Parent", bound=Optional["Entity"])
+_Self = TypeVar("_Self", bound="Entity[Any, Any]")
 
 
 class Entity(Generic[_Parent, _Child]):
@@ -202,7 +203,7 @@ def id(self, value):
         self._reset_full_id()
 
     def strictly_equals(
-        self, other: "Entity", compare_coordinates: bool = False
+        self: _Self, other: _Self, compare_coordinates: bool = False
     ) -> bool:
         """Compare this entity to the other entity for equality.
 
diff --git a/Bio/PDB/Residue.py b/Bio/PDB/Residue.py
--- a/Bio/PDB/Residue.py
+++ b/Bio/PDB/Residue.py
@@ -10,12 +10,14 @@
 from Bio.PDB.PDBExceptions import PDBConstructionException
 from Bio.PDB.Entity import Entity, DisorderedEntityWrapper
 
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, TypeVar
 
 if TYPE_CHECKING:
     from Bio.PDB.Atom import Atom
     from Bio.PDB.Chain import Chain
 
+_ResidueT = TypeVar("_ResidueT", bound="Residue")
+
 
 _atom_name_dict = {}
 _atom_name_dict["N"] = 1
@@ -44,7 +46,7 @@ def __repr__(self):
         return "<Residue %s het=%s resseq=%s icode=%s>" % full_id
 
     def strictly_equals(
-        self, other: "Residue", compare_coordinates: bool = False
+        self: _ResidueT, other: _ResidueT, compare_coordinates: bool = False
     ) -> bool:
         """Compare this residue to the other residue using a strict definition of equality.
 


Test patch: