Reputation: 175
Given the following code:
from typing import Tuple
class Grandparent:
items: Tuple[str, ...] = ()
class Parent(Grandparent):
items = ('foo',)
class Child(Parent):
items = ('foo', 'bar')
mypy
reports the following error:
error: Incompatible types in assignment (expression has type "Tuple[str, str]", base class "Parent" defined the type as "Tuple[str]")
Changing the code like this (specifying the same type again in the Parent
class) satisfies mypy
:
from typing import Tuple
class Grandparent:
items: Tuple[str, ...] = ()
class Parent(Grandparent):
items: Tuple[str, ...] = ('foo',)
class Child(Parent):
items = ('foo', 'bar')
Why do I need to re-specify the same type for items
at multiple places in the class hierarchy, given the assignment of items
in all places satisfies the same/original definition? And is there a way to avoid needing to do this?
Upvotes: 6
Views: 6934
Reputation: 64268
I believe this is a design choice that mypy made. In short, the crux of your question is this: when we override some attribute, do we want to use the same type as the parent, or use the new overridden type?
Mypy opted for the former -- it's arguably more intuitive in many cases. For example, if I have the following class hierarchy:
class Parent:
def foo(self, p1: int) -> None: ...
class Child(Parent):
def foo(self, p1: int, p2: str = "bar") -> None: ...
...it makes sense for Child.foo
to have a type of def (self: Child, p1: int, p2: str = ...) -> None
instead of directly inheriting the type of Parent.foo
, which is def (self: Parent, p1 : int) -> None
.
This way, everything still type checks if you do Child().foo(1, "a")
. More broadly, it's useful to be allowed to refine the parent type, with the only restriction being that the child still needs to follow the Liskov substitution principle after the refinement.
And if the rule is that the child definition wins for methods, it then makes sense to apply the same rule to attributes for the sake of consistency.
And as for how to work around this problem -- in your shoes, I'd probably either just settle for continuing to add the type annotation to each assignment. I don't think it's that big of a burden.
Alternatively, I might look into just collapsing the whole class hierarchy into a single class that accepts the appropriate tuple as a parameter in __init__
to try and side-step the need to hard-code something to begin with. But this may not be a viable solution for whatever you're trying to do.
Upvotes: 9