I have the following Foo
base class, and Bar
that inherits from it:
class Foo:
def __init__(self, x: int) -> None:
self._x = x
def do(self, x: int) -> None:
pass
class Bar(Foo):
pass
If I override Foo.do
in Bar
, and change the type of the x
parameter for something incompatible (that is, not more generic than int
), then Mypy returns an error - which is of course what I expect.
class Bar(Foo):
def do(self, x: str) -> None:
pass
Error:
test.py:10: error: Argument 1 of "do" is incompatible with supertype "Foo"; supertype defines the argument type as "int"
test.py:10: note: This violates the Liskov substitution principle
test.py:10: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
Found 1 error in 1 file (checked 1 source file)
However, if I override __init__
with an incompatible type for the argument, then Mypy accepts it:
class Bar(Foo):
def __init__(self, x: str) -> None:
self._x = 12
Mypy output:
Success: no issues found in 1 source file
It looks to me that overriding __init__
with incompatible types violates the LSP as well, since code like foo = Foo(12)
does not type-check if we replace Foo
with Bar
.
Why does Mypy accept that I override __init__
with an incompatible type? Is __init__
treated differently from other methods?
Also, is Mypy right in doing so? Am I correct that this last Bar
class violates the LSP?
The Liskov Substitution Principle is not generally considered to apply to constructor methods. If we were to treat constructor methods as part of an object's interface, it would make inheritance systems extremely difficult to manage in many situations, and lead to a whole host of other complications. See this question I posed on Software Engineering a while back.
The situation is complicated somewhat, however, by the fact that __init__
isn't really a constructor method (that would be __new__
) — it's an initialiser method, and it can be called multiple times on the same instance. It just "happens" to be the case that the initialiser method nearly always has the same signature as the constructor method.
Because of the way __init__
can be called multiple times on the same instance, just like a "normal" method that would be considered part of an object's interface, there's currently active discussion among the core devs regarding whether __init__
methods should be considered part of an object's interface in some respects.
To conclude: ¯\_(ツ)_/¯
Python is an extremely dynamic language, and that means reasoning about its type system can often be kind of strange.