Type inference for class member

Viewed 39

Using TypeVar and Generic, I can create classes with methods where types can be inferred , e.g.:

from typing import TypeVar, Generic

T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, content: T) -> None:
        self.content = content

Box(1)  # OK, inferred type is Box[int]

Is it possible to infer types of a class member? Let's say I have different types of athletes' stats:

from abc import ABC
from typing import Generic, TypeVar, Type


class Stats(ABC):
    pass


class BaseballStats(Stats):
    @property
    def batting_average(self) -> float:
        return 0.314


class FootballStats(Stats):
    @property
    def yards(self) -> int:
        return 314

And now I want to define an abstract class for an athlete:

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


class Athlete(ABC, Generic[S]):
    _stats_type: Type[S]

    @property
    def stats(self) -> S:
        return self._stats_type()

If I try to make a BaseballPlayer and have it infer the type by assigning to _stats_type, it won't:

class BaseballPlayer(Athlete):
    _stats_type = BaseballStats


bo = BaseballPlayer()
# reveal_type(bo.stats) --> gives `Any`; type was not inferred
print(bo.stats.batting_average)  # works.

If I specify the type of Athlete but don't assign _stats_type, it won't run.

class FootballPlayer(Athlete[FootballStats]):
    pass


bo = FootballPlayer()
# reveal_type(bo.stats) --> gives `FootballStats` because I explcitly called it out
print(bo.stats.yards)  # AttributeError: 'FootballPlayer' object has no attribute '_stats_type'

Is there an alternative that doesn't require that I put the type of statistics in two places? I.e., can I avoid doing this?

class FootballPlayer(Athlete[FootballStats]):
    _stats_type = FootballStats
1 Answers

First things first, you pretty much have to specify the type of Athlete when defining the subclasses, otherwise mypy will complain with the following:

error: Missing type parameters for generic type "Athlete"  [type-arg]

Now, contrary to your Box example, your issue is that you want to have the type at runtime (so that you can instantiate it in stats).

My experience (shared also by others, see e.g. beartype) is that using types hints at runtime is a bit clunky to say the least.

With that being said, you can probably do something like below, which gets the type at runtime from the Athlete your class is inheriting from. It is based on __orig_bases__ from PEP 560.

from typing import cast, get_args, get_origin


class Athlete(ABC, Generic[S]):
    @property
    def _stats_type(self) -> Type[S]:
        for orig_bases in self.__orig_bases__:
            if get_origin(orig_bases) == Athlete:
                return cast(Type[S], get_args(orig_bases)[0])
        raise RuntimeError  # should not happen

    @property
    def stats(self) -> S:
        return self._stats_type()


class FootballPlayer(Athlete[FootballStats]):
    pass

bo = FootballPlayer()
#reveal_type(bo.stats) --> gives `FootballStats`
print(bo.stats.yards)

Please note however the following warning about __orig_bases__ from the PEP:

NOTE: These two method names are reserved for use by the typing module and the generic types machinery, and any other use is discouraged.

Related