python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.13k stars 2.76k forks source link

Does Python type hinting allow covariant return types? #3566

Closed oTree-org closed 7 years ago

oTree-org commented 7 years ago

I would like to know if Python type hinting (the typing module) supports return type covariance, mainly for PyCharm's autocompletion.

Here are my base classes for my sports framework:

class BaseLeague:
    def get_teams(self) -> List[BaseTeam]:
        ...

    ...

class BaseTeam:
    def get_captain(self) -> BasePlayer:
        ...    

    def get_players(self) -> List[BasePlayer]:
        ...

    ...

class BasePlayer:
    team = None # type: BaseTeam

    ...

(There are many more methods I left out, like methods on BaseLeague returning BaseTeam/BasePlayer/BaseLeague objects, etc.)

I have multiple modules that subclass these 3 classes in parallel, and add/override methods and attributes.

In hockey/models.py, I have:

class League(BaseLeague):
    ...

class Team(BaseTeam):
    ...

class Player(BasePlayer):
    ...

And in football/models.py, I have the same thing:

class League(BaseLeague):
    ....

class Team(BaseTeam):
    ...

class Player(BasePlayer):
    ...

(I have 20+ other sports, like soccer, baseball, etc..)

In PyCharm, when I'm in football.models.Team and type self.get_captain()., PyCharm shows me the attributes for the base class BasePlayer, but I would like it to show me the attributes for the subclass football.models.Player. This seems closely related to return type covariance.

I have a feeling I need to use Generic and TypeVar like this:

Player = TypeVar('Player', covariant=True)
Team = TypeVar('Team', covariant=True)
League = TypeVar('League', covariant=True)

class BaseTeam(Generic[Player, Team, League]):
    def get_players(self) -> List[Player]:
        ...

And then in football.models I would do something like:

class Team(BaseTeam[Player, Team, League]):

...where Player, Team, League are references to the subclasses in the same module.

But it's not working (PyCharm is not showing any autocomplete at all), and I'm not sure if I'm using the right syntax.

I would like to get this working because my base classes are part of my framework's API, and my users subclass them hundreds of times, so I'd like them to benefit from PyCharm autocomplete, without having to override each method in their own code.)

Does anyone know if this is possible?

refi64 commented 7 years ago

I'm not sure if this is really covariance as much as some simple generic manipulation. Off the top of my head, something like this might do the trick:

T = TypeVar('T', bound='BaseLeague:),

class BaseLeague(Generic[T]):
    def get_teams(self) -> List[T]:
        ...

One problem with this method could be storing the League objects, but that's another story. ;)

oTree-org commented 7 years ago

Thanks for the reply....so, I wrote this:

from typing import TypeVar, Generic

PlayerTV = TypeVar('PlayerTV', bound='BasePlayer')
TeamTV = TypeVar('TeamTV', bound='BaseTeam')

class BaseTeam(Generic[PlayerTV, TeamTV]):
    def t(self) -> TeamTV: pass
    def p(self) -> PlayerTV: pass

class BasePlayer(Generic[PlayerTV, TeamTV]):
    def p(self) -> PlayerTV: pass

class Player(BasePlayer['Player', 'Team']):
    real_attr = 1

class Team(BaseTeam[Player, 'Team']):
    real_attr = 1

    def m1(self):
        a=self.t().real_attr
        a=self.t().fake_attr

    def m2(self):
        a=self.p().real_attr
        a=self.p().fake_attr

def f1(t: Team):
    a=t.t().real_attr
    a=t.t().fake_attr

When I run mypy mypy.py, I get: mypy.py:30: error: "Team" has no attribute "fake_attr"

I would like it to flag all the lines referencing fake_attr, but not real_attr. It seems to only be working for the function but neither of the methods.

refi64 commented 7 years ago

@oTree-org Mypy only type checks annotated methods. Since m1 and m2 don't have any type annotations, they aren't type checked. Try:

class Team(BaseTeam[Player, 'Team']):
    real_attr = 1

    def m1(self) -> None:
        a=self.t().real_attr
        a=self.t().fake_attr

    def m2(self) -> None:
        a=self.p().real_attr
        a=self.p().fake_attr
oTree-org commented 7 years ago

OK thank you :)