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 ofFullyTypedContainerand so it seems thatget_list_of_containers()[0].pis seen as typeAny.-> typing.List[FullyTypedContainer[Base]]: "coerces" everything to be of typeBase, (notBaseor one of its subclasses) and soget_list_of_containers()[0].pis 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 theCallabletype is actually contravariant w.r.t its argument types. A callable that takes aBaseargument can be used where we require a callable that takes aSubargument. - If neither of the above holds, then
GenTypeis invariant.
The Python List type is invariant, and the variance of Generic types depends on its TypeVar -- and TypeVars 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
BaseOrSubclasstype 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:
Listalways refers to the type for the builtinlist.Sequenceis any "list-like" type that defines__getitem__,__len__, a few other methods like__reverse__andcountetc. This also covers thetuple,str,bytestypes 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
