Bob Whitelock
Bob Whitelock

Reputation: 175

Why does mypy report incompatible types when child and parent class both satisfy grandparent type definition?

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

Answers (1)

Michael0x2a
Michael0x2a

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

Related Questions