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

3

5

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.

Ribble answered 10/4, 2020 at 18:25 Comment(4)
I don't think this will be possible, because in your case there is only one some_method (the one defined on SomeClass). Instances of SomeChildClass will resolve the method name to that same function object defined on SomeClass, they don't actually have their own separate implementation which you could annotate separately. Annotations are just an attribute on the function object, it can't really have different annotations depending on how it was resolved through the MRO.Termination
Ahh that makes sense @wim, thank you for the response. Do you have any ideas on the simplest way to update the annotation in the child class (SomeChildClass)?Ribble
Hmm, simplest way: rather than doing this RET_TYPE = float class attribute thing, just redefine the method in the child class?Termination
To add to the conversation: if you change the line to def some_method(self, some_input: str) -> RET_TYPE: (so replace the 'float' type hint with a 'RET_TYPE' hint, then the error indication in the view disappears (not in main unfortunately), at least in my PyCharm. So apparently PyCharm is clever enough to discover that RET_TYPE is different for every call.Crosson
R
2

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"
    )
Ribble answered 15/4, 2020 at 22:9 Comment(1)
That works if it fits your use case, but note that if you want to have a subclass hierarchy (as suggested in the question) like class SomeChildClass2(SomeChildClass1):, you end up where you started basically.Rachmaninoff
R
3

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.

Rachmaninoff answered 14/4, 2020 at 17:53 Comment(3)
Thank you @KevinLanguasco for the answer! I am not the best programmer, can you please clarify why you needed to use bound=complex with S = TypeVar('S', bound=complex)?Ribble
Oh that actually isn't needed! It's just saying that every concrete type to replace S must be a subtype of complex. So if you wrote, say, SomeMethodImplementor[str] it would send a warning, since str is not a subtype of complex. Just additional type safety :) python.org/dev/peps/pep-0484/… (I'm using the fact that int is a subtype of float, which is a subtype of complex)Rachmaninoff
Okay @KevinLanguasco I am awarding you the bounty because your answer filled in a lot of holes I was missing. I was able to find a hybrid solution (which I posted) that also doesn't generate errors in mypy, if you are curious/have any thoughtsRibble
W
2

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.

Wapiti answered 14/4, 2020 at 15:27 Comment(1)
Thank you @AntonPomieshchenko for your answer!Ribble
R
2

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"
    )
Ribble answered 15/4, 2020 at 22:9 Comment(1)
That works if it fits your use case, but note that if you want to have a subclass hierarchy (as suggested in the question) like class SomeChildClass2(SomeChildClass1):, you end up where you started basically.Rachmaninoff

© 2022 - 2024 — McMap. All rights reserved.