Rationale behind dataclasses using a decorator

Another option that some libraries take when extending dataclass is to make Dataclass a plain base class. This has a number of benefits over the decorator approach, including isinstance checking, and the ability to add methods and class attributes. You can use a plain base class thanks to __init_subclass__.

I always thought the decorator approach was chosen over base classes because dataclasses was borrowed from attrs?

I think using isinstance is an anti-pattern. Why would you need to know if two unrelated classes are dataclasses? It’s like namedtuple: there’s no common base class for a reason.

2 Likes

For ordinary dataclasses, it would come in handy since it would allow you to properly type annotate and type check dataclasses.replace and dataclasses.fields.

I also have written functions that accept a dataclass and iterate over the fields. So I would like to annotate such a function properly and do type checking within that function. isinstance is the canonical way to check a type.

For specialized dataclass, there’s even more reason to want to check. For example, flax.struct.dataclass is a specialized dataclass used in Flax. Many functions only accept “PyTrees” (a concept in Jax), which is basically most scalar types and flax.struct.dataclass. So it would really help to be able to check.

Specialized dataclasses can add behavior, so I think it makes sense to want to check if an object exposes that behavior.

2 Likes

We’re way off topic here, but for specialized base classes you should use your own base class.

Yes, totally agree on both points :smiley:

At the risk of straying further OT the attrs plug-in for mypy “injects” an attrs-specific dunder to attrs classes. The dunder’s protocol allows typing users to benefit from the same typing guarantees that subclassing would provide. The same I imagine would hold true for dataclasses.

Yeah, this discussion has deviated too much from Protocol and its @protocol decorator. I guess you guys like dataclasses a bit too much? :joy:

1 Like

Here’s my initial @protocol decorator implementation.

from typing import Protocol


class protocol:
    """A @protocol decorator that defines a Protocol-based class."""

    def __init__(self, class_type):
        self.class_type = class_type

    def __call__(self):
        new_class_name = self.class_type.__name__
        new_class_base = Protocol, self.class_type
        new_class_dict = dict(self.class_type.__dict__)
        new_class_type = type(new_class_name, new_class_base, new_class_dict)
        return new_class_type()


# Usage example

@protocol
class MathProtocol:
    def sum(self, x: int, y: int, /) -> int:
        ...

    def subtract(self, x: int, y: int, /) -> int:
        ...


class MathOperations:
    def sum(self, x: int, y: int, /) -> int:
        return x + y

    def subtract(self, x: int, y: int, /) -> int:
        return x - y