0

I'm trying to create classes that can be saved to a JSON file. The idea is to convert my object to a dataclass that stores only the important attributes. I'll then use something from json or dataclass_wizard to save the dataclass to a file/reconstruct the object later.

Here's the problem: I want to have an alternate constructor to create the object from the dataclass. I can't overload __init__(), so I'm using a @classmethod. Because the from_briefcase() method is overridden in BiggerBusinessMan, I get a mypy error that this violates the LSP. I can see why I get this error, but is there a way to tell Python that from_briefcase() is meant to be an alternate constructor and not part of the interface? As far as I understand, changing the arguments of __init__() would not violate the LSP, so I want from_briefcase() to be treated similarly.

from dataclasses import dataclass
from typing import Self


@dataclass
class Briefcase:
    item1: str
    item2: str


@dataclass
class BiggerBriefcase(Briefcase):
    item3: str
    # also has `item1` and `item2`


class BusinessMan:
    def __init__(self):
        self.item1 = ""
        self.item2 = ""

    @classmethod
    def from_briefcase(cls, briefcase: Briefcase) -> Self:
        businessman = cls()
        businessman.item1 = briefcase.item1
        businessman.item2 = briefcase.item2
        return businessman


class BiggerBusinessMan(BusinessMan):
    def __init__(self):
        super().__init__()
        self.item3 = ""

    @classmethod
    def from_briefcase(cls, briefcase: BiggerBriefcase) -> Self:  # mypy error on this line
        # I want to be able to use the base class constructor
        businessman = super().from_briefcase(briefcase)
        businessman.item3 = briefcase.item3
        return businessman


# these two lines would really be run by some sort of JSON loader to create the dataclasses
basic_briefcase = Briefcase("notes", "cards")
bigger_briefcase = BiggerBriefcase("pens", "pencils", "letters")

# I'm not trying to use `from_briefcase()` polymorphically, it's supposed to be like a constructor
basic_businessman = BusinessMan.from_briefcase(basic_briefcase)
bigger_businessman = BiggerBusinessMan.from_briefcase(bigger_briefcase)

Also, I know I could use a dict instead of a dataclass to solve this issue, but I want stronger typing and to not have to keep track of the correct dictionary keys.

Worst case I can throw a # type: ignore on there but that seems silly. I could also use __init__(self, briefcase), but I would like to be able to create the object without needing a Briefcase instance.

1
  • @STerliakov Dang, I can't believe I didn't find that. That's perfect! I'll post an answer and close. Thank you! Commented Jun 1 at 2:48

1 Answer 1

0

Thanks to @STerliakov for finding a related post!

The solution is generics. So the BusinessMan declaration becomes

class BusinessMan[BriefcaseType: Briefcase]:
    # snip

    @classmethod
    def from_briefcase(cls, briefcase: BriefcaseType) -> Self:
        # snip

which indicates that from_briefcase() takes a BriefcaseType, which is any subclass of Briefcase.

The declaration for BiggerBusinessMan becomes

class BiggerBusinessMan(BusinessMan[BiggerBriefcase]):
    # snip

    @classmethod
    def from_briefcase(cls, briefcase: BiggerBriefcase) -> Self:
        # snip

which says that BiggerBusinessMan inherits from the variant of BusinessMan where BriefcaseType is BiggerBriefcase.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.