Let's say I want to create an abstract base class called Document
. I want the type checker to guarantee that all its subclasses implement a class method called from_paragraphs
, which constructs a document from a sequence of Paragraph
objects. However, a LegalDocument
should only be constructable from LegalParagraph
objects, and an AcademicDocument
- only from AcademicParagraph
objects.
My instinct is to do it like so:
from abc import ABC, abstractmethod
from typing import Sequence
class Document(ABC):
@classmethod
@abstractmethod
def from_paragraphs(cls, paragraphs: Sequence["Paragraph"]):
pass
class LegalDocument(Document):
@classmethod
def from_paragraphs(cls, paragraphs: Sequence["LegalParagraph"]):
return # some logic here...
class AcademicDocument(Document):
@classmethod
def from_paragraphs(cls, paragraphs: Sequence["AcademicParagraph"]):
return # some logic here...
class Paragraph:
text: str
class LegalParagraph(Paragraph):
pass
class AcademicParagraph(Paragraph):
pass
However, Pyright complains about this because from_paragraphs
on the derived classes violates the Liskov substitution principle. How do I make sure that each derived class implements from_paragraphs
for some kind of Paragraph
?
from_paragraphs()
, why does the base class implement that method at all? Couldn't you just omit that method from the base class? – RowlandDocument.from_paragraphs
should be generic.Document.from_paragraphs
doesn't need to work with a list of arbitrary mix ofParagraph
subclasses; rather the overrides each need to work with a homogenous list of a particular subtype ofParagraph
. – IntricacyParagraph
, so that the type checker approves of some other function which takes aDocument
and uses its.from_paragraph
method. I maybe made this too confusing by using classmethods but I'll keep them in the example to not derail the discussion. – PlotSequence["LegalParagraph"]
is still a narrowing ofSequence[P]
for inheritance purposes. I think applying LSP to class methods is already a bit dicey, since although you can call a class method using an instance (it just passes the class of the instance, not the instance itself as the implicit argument), the behavior of the method typically is independent of the that instance. (1/2) – Intricacymypy
is correct in flagging this as an LSP violation in the first place, because now I am curious. (2/2) – IntricacyMethod "from_paragraphs" overrides class "Document" in an incompatible manner
(...and I use PyRight ;) ). Either way, you could have some function accepttype[Document]
and then expect to be able to construct that typefrom_paragraphs
, so I suppose this makes sense to call an LSP violation 🤷 – PlotDocument
and its subclasses all have, say, a class attributeParagraph
that's assigned the appropriateParagraph
subtype, and then you can reference that using some sort of syntax likedef from_paragraphs(cls: Type[T], paragraphs: List[T.Paragraph])
.T
here would be something likeTypeVar('T', bound=Document)
. – Intricacy