A situation I find a lot is that I have a method in a superclass that is inherited by many subclasses that should each have more specific return types for the method. I’m trying to establish the best method of annotating this.
A simple example would be:
class Base:
def generic_method(self) -> Base:
# implementation here is inherited to all
class Child(Base): pass
class GrandChild(Child): pass
b = Base()
c = Child()
gc = GrandChild()
assert isinstance(b.generic_method(), Base)
assert isinstance(c.generic_method(), Child)
assert isinstance(gc.generic_method(), Child)
Now I want to add annotations somehow to say that Child.generic_method()
and GrandChild.generic_method()
both return Child
rather than Base
. I don’t know where to add these annotations because the method is only implemented in Base
and is inherited by the other classes. There are other constraints involved that ensure that the appropriate types are returned even though the implementation of generic_method
is the same for all classes.
I think if I was using stub files I could just add annotations for Child.generic_method
in the stub file without adding any implementation in the actual class but there doesn’t seem to be a way to achieve the same effect with inline annotations.
This example looks almost like a case for Self
but that doesn’t work because it is not true that GrandChild.generic_method()
always returns a GrandChild
.
There is a way that works here which is to add a redundant method in Child
:
class Child(Base):
def generic_method() -> Child:
"""Duplicated docstring."""
# redundant super call:
return super().generic_method()
For all the same reasons that you would generally want to use Self
when possible this is not great: there are several Child
classes and many inherited methods and I don’t want to duplicate all the docstrings and have redundant method calling at runtime. I also don’t want to spread dummy implementations around for a method that should really be defined in one place only.
One thing I have tried which is also not great but at least keeps everything defined in one place is overloading on self
in the base class:
from __future__ import annotations
from typing import overload
class Base:
@overload
def generic_method(self: Child) -> Child: ...
@overload
def generic_method(self: Base) -> Base: ...
def generic_method(self) -> Base:
assert False
class Child(Base): pass
class GrandChild(Child): pass
b = Base()
c = Child()
gc = GrandChild()
reveal_type(b.generic_method()) # Base
reveal_type(c.generic_method()) # Child
reveal_type(gc.generic_method()) # Child
This is accepted by pyright although mypy reports an error:
The erased type of self “Child” is not a supertype of its class “Base”
It seems that mypy doesn’t like overloading on the type of self. although it does still infer the types correctly as reported by reveal_type
which is the main thing I want.
Are there any other/better ways of doing this?