pchlyx
pchlyx

Reputation: 21

How to type hint factories in subclasses

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

Answers (1)

dROOOze
dROOOze

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 @classmethods. 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

Related Questions