How to subclass a frozen dataclass

Question:

I have inherited the Customer dataclass. This identifies a customer in the customer DB table.

Customer is used to produce summary statistics for transactions pertaining to a given customer. It is hashable, hence frozen.

I require a SpecialCustomer (a subclass of Customer) it has an extra property: special_property. Most of the properties inherited from Customer will be set to fixed values. This customer does not exist in the Customer table.

I wish to utilise code which has been written for Customer. Without special_property we will not be able to distinguish between special customers.

How do I instantiate SpecialCustomer?

Here is what I have. I know why this doesn’t work. Is there some way to do this?:

from dataclasses import dataclass


@dataclass(frozen=True, order=True)
class Customer:
    property: str


@dataclass(frozen=True, order=True)
class SpecialCustomer(Customer):

    special_property: str = Field(init=False)

    def __init__(self, special_property):
        super().__init__(property="dummy_value")
        self.special_property = special_property

s = SpecialCustomer(special_property="foo")

Error:

E   dataclasses.FrozenInstanceError: cannot assign to field 'special_property'

<string>:4: FrozenInstanceError
Asked By: GlaceCelery

||

Answers:

Why not like this?

from dataclasses import dataclass

from pydantic.dataclasses import dataclass as pydantic_dataclass


@dataclass(frozen=True, order=True)
class Customer:
    prop: str


@pydantic_dataclass(frozen=True, order=True, kw_only=True)
class SpecialCustomer(Customer):
    special_prop: str
    prop: str = "dummy_value"


print(SpecialCustomer(special_prop="foo"))

Output: SpecialCustomer(prop='dummy_value', special_prop='foo')

Problem is that without kw_only=True we cannot have a non-default field after a default one and with the weird approach taken by dataclasses, prop is still considered before special_prop, even though it is re-declared after it…

Dataclasses are just very restrictive. Basically, if you want anything other vanilla, you’ll have a bad time. If you were willing/able to switch to something like attrs instead, those are much more flexible and also lightweight. Normal Pydantic models too of course, but they are less light-weight.


If the problem with my suggested solution is that it still allows users of the SpecialCustomer class to set arbitrary values for prop, you could prevent that with an additional check in __post_init__. That would of course be annoying, if there are many fields that should be fixed, but I fail to see any other way to construct this.

Something like this:

...

@pydantic_dataclass(frozen=True, order=True, kw_only=True)
class SpecialCustomer(Customer):
    special_prop: str
    prop1: str = "dummy_value"
    prop2: int = 123
    prop3: tuple[float, float] = (3.14, 0.)

    def __post_init__(self) -> None:
        assert self.prop1 == "dummy_value"
        assert self.prop2 == 123
        assert self.prop3 == (3.14, 0.)


print(SpecialCustomer(special_prop="foo"))
try:
    SpecialCustomer(prop1="something", special_prop="bar")
except AssertionError as e:
    print("No! Bad user.")

Alternatively, since this is a Pydantic class, you could define validators for the fixed fields that do essentially the same thing.


PS: Possible attrs solution

from dataclasses import dataclass

from attrs import define, field


@dataclass(frozen=True, order=True)
class Customer:
    prop1: str
    prop2: int
    prop3: tuple[float, float]


@define(frozen=True, order=True)
class SpecialCustomer(Customer):
    prop1: str = field(default="dummy_value", init=False)
    prop2: int = field(default=123, init=False)
    prop3: tuple[float, float] = field(default=(3.14, 0.), init=False)
    special_prop: str


if __name__ == "__main__":
    import json
    from attrs import asdict
    s = SpecialCustomer("foo")
    print(json.dumps(asdict(s), indent=4))
    print(isinstance(s, Customer))
    print(hash(s))
    try:
        SpecialCustomer(prop1="abc", special_prop="bar")
    except TypeError as e:
        print(repr(e))

Output:

{
    "prop1": "dummy_value",
    "prop2": 123,
    "prop3": [
        3.14,
        0.0
    ],
    "special_prop": "foo"
}
True
6587449294214520366
TypeError("SpecialCustomer.__init__() got an unexpected keyword argument 'prop1'")
Answered By: Daniil Fajnberg