1

I have a Generic base class that returns itself in one method (get_self). I have type hinted it as such.

I then have a child class of that base class that passes in a type arg for the Generic. In that child class, I call get_self. I would like to update the type hint to just be the name of the child class.

However, mypy==0.782 is reporting error: Incompatible return value type (got "Foo[Bar]", expected "DFoo") [return-value]. Is there some way to accomplish this?


**Edit**

I decided to re-explain the question upon further reflection. Sorry in advance for the verbosity.

  1. Base class (Foo) has a method (get_self) type hinted to return an instance of itself
  2. Child class (DFoo) does not override the method
  3. Child class then uses the (get_self) method
    • And knows that the return type will actually be of the child class (DFoo)
  4. However, static type checkers (ex: mypy) do not know that the child class's method will actually return an object of the child class, as they are using the type hint from the base class

So my question may not be possible without re-declaring the method (get_self) in the child class with a new type hint.

I could make the return of get_self be a TypeVar. However, since the base class Foo is already a Generic, that's currently not possible, because it would require the "Higher-Kinded TypeVars" mentioned in python/typing Higher-Kinded TypeVars #548.


Sample Script

I hope this clears up what I am trying to get at.

from __future__ import annotations

from typing import Generic, TypeVar, cast

T = TypeVar("T")

class Foo(Generic[T]):
    def get_self(self) -> Foo[T]:
        # Other stuff happens here before the return
        return self

class Bar:
    pass

class DFoo(Foo[Bar]):
    def do_something_get_self(self) -> DFoo:
        # mypy error: Incompatible return value type (got "Foo[Bar]", 
        # expected "DFoo")
        return self.get_self()

class DFooCast(Foo[Bar]):
    def do_something_get_self(self) -> DFooCast:
        # This works, but I don't like this method. I don't want to use `cast`
        # all over the place.
        return cast(DFooCast, self.get_self())

class DFooNoUpdatedTypeHint(Foo[Bar]):
    def do_something_get_self(self) -> Foo[Bar]:
        # mypy doesn't error here, but later on it will raise an error 
        # when using method's added in Foo subclasses
        return self.get_self()

    def dfoo_adds_method(self) -> None:
        """DFoo also has additional methods."""

dfoo = DFooNoUpdatedTypeHint()
dfoo.do_something_get_self().dfoo_adds_method()  # error: "Foo[Bar]" has no attribute "dfoo_adds_method"

And here is the complete mypy output:

path/to/ret_type_type_t_subclass.py: note: In member "do_something_get_self" of class "DFoo":
path/to/ret_type_type_t_subclass.py: error: Incompatible return value type (got "Foo[Bar]", expected "DFoo")  [return-value]
path/to/ret_type_type_t_subclass.py: note: At top level:
path/to/ret_type_type_t_subclass.py: error: "Foo[Bar]" has no attribute "dfoo_adds_method"  [attr-defined]

Versions

Python==3.8.5
mypy==0.782
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
  • `Foo[Bar]` isn't a class; it's a type hint. – chepner Sep 21 '20 at 15:42
  • Are you saying `Foo` shouldn't subclass `Foo[Bar]`? My understanding is when entering `Foo[Bar]`, it's the class `Foo` with its type argument = `Bar`. – Intrastellar Explorer Sep 21 '20 at 15:53
  • Classes don't take a type argument; type hints do. You should only be inheriting from `Foo` itself. – chepner Sep 21 '20 at 15:54
  • Check [this mypy comment](https://github.com/python/mypy/issues/4148#issuecomment-338492577). One can pass type args during inheritance, for specificity. If I get rid of the `[Bar]` from the inheritance, the error becomes less specific and remains present: `error: Incompatible return value type (got "Foo[Any]", expected "DFoo")`. Maybe I am missing something, but I don't think the type arg for `DFoo` is creating the problem in this case. – Intrastellar Explorer Sep 21 '20 at 16:11
  • @IntrastellarExplorer How about `def do_something_get_self(self) -> Foo`? It's elegant, polymorphic, doesn't require sophisticated definitions or assignments. Plus it describes the return type accurately. – Aviv Yaniv Sep 23 '20 at 19:01
  • Good question @AvivYaniv and thank you for asking. It's because the subclass `DFoo` adds methods, and without a `DFoo` type hint, `mypy` will later raise an error. I have updated the question to include this – Intrastellar Explorer Sep 23 '20 at 20:18
  • Well, if I take the code "as is", considering that there is a super method that acts on the object and returns `self`, and we want in our class to have another function that activates the super method and returns self too; we can do something like that: `def do_something_get_self(self) -> DFoo: self.get_self() return self` and avoid the problem of type hinting of the super `get_self`. – Aviv Yaniv Sep 23 '20 at 20:46
  • This is not a very common scenario, so it's possible it's a bug in mypy. I'd recommend opening an issue in their Issue Tracker. Even in case it's not a bug, it's the best place where to get an advice about it. – yedpodtrzitko Sep 24 '20 at 01:34
  • @IntrastellarExplorer I’m sure you make simplifications to make the example readable and in context for us to grasp. I'd be happy to hear your thoughts regarding my suggestion, and if there are any complications to consider :) – Aviv Yaniv Sep 24 '20 at 13:26
  • Alright I updated the question with a more detailed explanation of the problem @AvivYaniv. Let me know if you come up with anything, but I think I may need to revisit the overall design to avoid the problem I am hitting. – Intrastellar Explorer Sep 25 '20 at 01:17
  • 1
    The `def do_something_get_self(self) -> DFoo: self.get_self() return self` suggestion should suffice; by invoking super's `get_self` and then returning `self` we can overpass the type checking @IntrastellarExplorer what do you think? – Aviv Yaniv Sep 25 '20 at 07:01
  • Yes @AvivYaniv that is a viable workaround for the problem. However, the point of `get_self` in the parent class `Foo` was so it could be used to get a self instance, not be skirted around just because `mypy` raised an error. If you see the selected answer, it enables one to `return self.get_self()` directly. – Intrastellar Explorer Sep 25 '20 at 21:10

1 Answers1

1

To solve this problem, just type your get_self function as def get_self(self: S) -> S where S is some type var.

The following program will then type check cleanly:

from __future__ import annotations

from typing import Generic, TypeVar, cast

T = TypeVar("T")

# This can also be just 'S = TypeVar("S")', but that would mean
# we won't be able to use any methods of Foo inside of get_self.
S = TypeVar("S", bound="Foo")


class Foo(Generic[T]):
    def get_self(self: S) -> S:
        return self

class Bar:
    pass


class DFoo(Foo[Bar]):
    def do_something_get_self(self) -> DFoo:
        return self.get_self()

    def dfoo_adds_method(self) -> None:
        pass


dfoo = DFoo()
dfoo.do_something_get_self().dfoo_adds_method()

The reason this works is because it's always possible to override the default type of self. While it's normally automatically given the type of the current class, PEP 484 does not actually mandate you stick with this default.

So, we make it generic instead to ensure the output type will always match whatever the current subtype is.

See https://mypy.readthedocs.io/en/stable/generics.html#generic-methods-and-generic-self for more details about this interaction.

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • The key is actually type hinting `self: S`, which took me a second to realize. And thank you @Michael0x2a yet again! In my actual use case, that solved the problem! – Intrastellar Explorer Sep 25 '20 at 21:05