Reputation: 21
What is the correct way to type hint such subclassed factories, like those in the following popular pattern? The Liskov Substitution Principle originally only applies to instances, not classes, so I don't understand why mypy seems to restrict the typing of class methods in subclasses.
from typing import Any, Self
class A:
def __init__(self, a: int):
self.a = a
@classmethod
def create(cls, a: int, **keywords: Any) -> Self:
return cls(a)
class B(A):
def __init__(self, a: int, b: int):
super().__init__(a)
self.b = b
@classmethod
def create(cls, a: int, b: int, **keywords: Any) -> Self:
return cls(a, b)
Running mypy (1.8.0) gives:
test_factory.py:19: error: Signature of "create" incompatible with supertype "A" [override]
test_factory.py:19: note: Superclass:
test_factory.py:19: note: @classmethod
test_factory.py:19: note: def create(cls, a: int, **keywords: Any) -> B
test_factory.py:19: note: Subclass:
test_factory.py:19: note: @classmethod
test_factory.py:19: note: def create(cls, a: int, b: int, **keywords: Any) -> B
Found 1 error in 1 file (checked 1 source file)
Upvotes: 2
Views: 85
Reputation: 3608
The Liskov Substitution Principle originally only applies to instances, not classes, so I don't understand why mypy seems to restrict the typing of class methods in subclasses.
That would only be true if instances didn't have access to @classmethod
s. The only thing that is allowed to violate LSP is __init__
/ __new__
for practicality reasons, even though they are implicitly accessible on an instance (via type(self)(<...>)
). Nothing else, not even the "Pythonic" @classmethod
-decorated alternative constructor methods like what you have here, is allowed to violate it.
To solve this, you can loosely type the @classmethod
in the base class (mypy Playground):
from __future__ import annotations
import typing_extensions as t
if t.TYPE_CHECKING:
import collections.abc as cx
class A:
def __init__(self, a: int) -> None: ...
# A positional `int` followed by any number of positional and keyword arguments
create: t.ClassVar[cx.Callable[t.Concatenate[int, ...], A]] = classmethod(lambda cls, a, **keywords: cls(a)) # type: ignore[assignment]
class B(A):
def __init__(self, a: int, b: int) -> None: ...
@classmethod
def create(cls, a: int, b: int, **keywords: t.Any) -> t.Self: ... # OK
class C(A):
def __init__(self, a: str, b: int) -> None: ...
@classmethod
def create(cls, a: str, b: int, **keywords: t.Any) -> t.Self: ... # mypy: Incompatible types in assignment
Upvotes: 0