I'm just getting into the more sophisticated typehinting stuff in Python, in particular, typing.Generic
.
Say I have a base class and a subclass of that:
class Base:
def base_method(self):
pass
class Sub(Base):
def sub_method(self):
pass
Now I want to create a class that will have an instance variable that can be "a Base
or any subclass thereof". To do this, I must make use of typing.TypeVar
and typing.Generic
:
class FullyTypedContainer(Generic[BaseOrSubclass]):
def __init__(self, p: BaseOrSubclass):
self._p = p
@property
def p(self) -> BaseOrSubclass:
return self._p
This works great; the type of p
is "passed along" via BaseOrSubclass
so that language servers like PyLance will see that FullTypedContainer(Sub()).p
has a method called sub_method()
but FullyTypedContainer(Base()).p
does not.
So what's the typehint for this function?
def get_list_of_containers():
return [FullyTypedContainer(Sub()), FullyTypedContainer(Base())]
-> typing.List[FullyTypedContainer]
: doesn't specify the type ofFullyTypedContainer
and so it seems thatget_list_of_containers()[0].p
is seen as typeAny
.-> typing.List[FullyTypedContainer[Base]]
: "coerces" everything to be of typeBase
, (notBase
or one of its subclasses) and soget_list_of_containers()[0].p
is seen as never having the methodsub_method()
.-> typing.List[FullyTypedContainer[typing.Union[Base, Sub]]
: seems to be the best option, but would require me to manually maintain a list of every subclass ofBase
.
CodePudding user response:
There are two issues here, and both issues are related to variance of generic types: both List
and your custom generic type FullyTypedContainer
are invariant.
Assume Sub
is a subtype of Base
. Given a generic type GenType
:
- If
GenType[Sub]
is a subtype ofGenType[Base]
, then it is covariant. A lot of containers are intuitively covariant. - If
GenType[Base]
is a subtype ofGenType[Sub]
, then it is contravariant. This sounds counterintuitive, but theCallable
type is actually contravariant w.r.t its argument types. A callable that takes aBase
argument can be used where we require a callable that takes aSub
argument. - If neither of the above holds, then
GenType
is invariant.
The Python List
type is invariant, and the variance of Generic
types depends on its TypeVar
-- and TypeVar
s by default are invariant. So to correctly type annotate your function, you need to make two changes:
- Use a covariant sequence type, e.g.
Sequence
. - Make the
BaseOrSubclass
type variable covariant as well.
In code, it would look like this:
BaseOrSubclass = TypeVar("BaseOrSubclass", bound=Base, covariant=True)
class FullyTypedContainer(Generic[BaseOrSubclass]):
def __init__(self, p: BaseOrSubclass): ...
def get_list_of_containers() -> Sequence[FullyTypedContainer[Base]]:
return [FullyTypedContainer(Sub()), FullyTypedContainer(Base())]
Here's a more complete example on mypy-play: https://mypy-play.net/?mypy=latest&python=3.10&gist=85af8d25521c9d4b8454719685b37fc8
A note on List
vs. Sequence
:
Technically List
is not equivalent to Sequence
:
List
always refers to the type for the builtinlist
.Sequence
is any "list-like" type that defines__getitem__
,__len__
, a few other methods like__reverse__
andcount
etc. This also covers thetuple
,str
,bytes
types and such.
CodePudding user response:
lists are homogenous in their type: all elements have the same static type. The best you can do is List[FullyTypedContainer[Base]]
or List[FullyTypedContainer[Union[Base, Sub]]
. Or define a custom collection that allows you to constrain particular contents to particular types and use that instead of list