'Python overriding type hint on a method's return in child class, without redefining method signature

I have a base class with a type hint of float on a method's return.

In the child class, without redefining the signature, can I somehow update the type hint on the method's return to be int?


Sample Code

#!/usr/bin/env python3.6


class SomeClass:
    """This class's some_method will return float."""

    RET_TYPE = float

    def some_method(self, some_input: str) -> float:
        return self.RET_TYPE(some_input)


class SomeChildClass(SomeClass):
    """This class's some_method will return int."""

    RET_TYPE = int


if __name__ == "__main__":
    ret: int = SomeChildClass().some_method("42"). # 
    ret2: float = SomeChildClass().some_method("42")

My IDE complains about a type mismatch:

pycharm expected type float

This is happening because my IDE is still using the type hint from SomeClass.some_method.


Research

I think the solution might be to use generics, but I am not sure if there's a simpler way.

Python: how to override type hint on an instance attribute in a subclass?

Suggests maybe using instance variable annotations, but I am not sure how to do that for a return type.



Solution 1:[1]

Okay so I was able to play around and combine the answers from @AntonPomieshcheko and @KevinLanguasco to come up with the a solution where:

  • My IDE (PyCharm) can properly infer return type
  • mypy reports if a there's a mismatch of types
  • Doesn't error at runtime, even if type hints indicate a mismatch

This is exactly the behavior I had wanted. Thank you so much to everyone :)

#!/usr/bin/env python3

from typing import TypeVar, Generic, ClassVar, Callable


T = TypeVar("T", float, int)  # types supported


class SomeBaseClass(Generic[T]):
    """This base class's some_method will return a supported type."""

    RET_TYPE: ClassVar[Callable]

    def some_method(self, some_input: str) -> T:
        return self.RET_TYPE(some_input)


class SomeChildClass1(SomeBaseClass[float]):
    """This child class's some_method will return a float."""

    RET_TYPE = float


class SomeChildClass2(SomeBaseClass[int]):
    """This child class's some_method will return an int."""

    RET_TYPE = int


class SomeChildClass3(SomeBaseClass[complex]):
    """This child class's some_method will return a complex."""

    RET_TYPE = complex


if __name__ == "__main__":
    some_class_1_ret: float = SomeChildClass1().some_method("42")
    some_class_2_ret: int = SomeChildClass2().some_method("42")

    # PyCharm can infer this return is a complex.  However, running mypy on
    # this will report (this is desirable to me):
    # error: Value of type variable "T" of "SomeBaseClass" cannot be "complex"
    some_class_3_ret = SomeChildClass3().some_method("42")

    print(
        f"some_class_1_ret = {some_class_1_ret} of type {type(some_class_1_ret)}\n"
        f"some_class_2_ret = {some_class_2_ret} of type {type(some_class_2_ret)}\n"
        f"some_class_3_ret = {some_class_3_ret} of type {type(some_class_3_ret)}\n"
    )

Solution 2:[2]

The following code works nicely on PyCharm. I added the complex case to make it clearer.

I basically extracted the method to a generic class and then used it as a mixin to each subclass. Please use with extra care, since it seems to be rather non-standard.

from typing import ClassVar, Generic, TypeVar, Callable


S = TypeVar('S', bound=complex)


class SomeMethodImplementor(Generic[S]):
    RET_TYPE: ClassVar[Callable]

    def some_method(self, some_input: str) -> S:
        return self.__class__.RET_TYPE(some_input)


class SomeClass(SomeMethodImplementor[complex]):
    RET_TYPE = complex


class SomeChildClass(SomeClass, SomeMethodImplementor[float]):
    RET_TYPE = float


class OtherChildClass(SomeChildClass, SomeMethodImplementor[int]):
    RET_TYPE = int


if __name__ == "__main__":
    ret: complex = SomeClass().some_method("42")
    ret2: float = SomeChildClass().some_method("42")
    ret3: int = OtherChildClass().some_method("42")
    print(ret, type(ret), ret2, type(ret2), ret3, type(ret3))

If you change, for example, ret2: float to ret2: int, it will correctly show a type error.

Sadly, mypy does show errors in this case (version 0.770),

otherhint.py:20: error: Incompatible types in assignment (expression has type "Type[float]", base class "SomeClass" defined the type as "Type[complex]")
otherhint.py:24: error: Incompatible types in assignment (expression has type "Type[int]", base class "SomeClass" defined the type as "Type[complex]")
otherhint.py:29: error: Incompatible types in assignment (expression has type "complex", variable has type "float")
otherhint.py:30: error: Incompatible types in assignment (expression has type "complex", variable has type "int")

The first errors can be "fixed" by writing

    RET_TYPE: ClassVar[Callable] = int

for each subclass. Now, the errors reduce to

otherhint.py:29: error: Incompatible types in assignment (expression has type "complex", variable has type "float")
otherhint.py:30: error: Incompatible types in assignment (expression has type "complex", variable has type "int")

which are precisely the opposite of what we want, but if you only care about PyCharm, it doesn't really matter.

Solution 3:[3]

You can just use something like that:

from typing import TypeVar, Generic


T = TypeVar('T', float, int) # types you support


class SomeClass(Generic[T]):
    """This class's some_method will return float."""

    RET_TYPE = float

    def some_method(self, some_input: str) -> T:
        return self.RET_TYPE(some_input)


class SomeChildClass(SomeClass[int]):
    """This class's some_method will return int."""

    RET_TYPE = int


if __name__ == "__main__":
    ret: int = SomeChildClass().some_method("42")
    ret2: float = SomeChildClass().some_method("42")

But there is one problem. That I do not know how to solve. For SomeChildClass method some_method IDE will show generic hint. At least pycharm(I suppose you this it) does not show it as error.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Neuron - Freedom for Ukraine
Solution 2
Solution 3 Anton Pomieshchenko