Say you want to wrap the dataclass
decorator like so:
from dataclasses import dataclass
def something_else(klass):
return klass
def my_dataclass(klass):
return something_else(dataclass(klass))
How should my_dataclass
and/or something_else
be annotated to indicate that the return type is a dataclass?
See the following example on how the builtin @dataclass
works but a custom @my_dataclass
does not:
@dataclass
class TestA:
a: int
b: str
TestA(0, "") # fine
@my_dataclass
class TestB:
a: int
b: str
TestB(0, "") # error: Too many arguments for "TestB" (from mypy)
There is no feasible way to do this prior to PEP 681.
A dataclass
does not describe a type but a transformation. The actual effects of this cannot be expressed by Python's type system – @dataclass
is handled by a MyPy Plugin which inspects the code, not just the types. This is triggered on specific decorators without understanding their implementation.
dataclass_makers: Final = {
'dataclass',
'dataclasses.dataclass',
}
While it is possible to provide custom MyPy plugins, this is generally out of scope for most projects. PEP 681 (Python 3.11) adds a generic "this decorator behaves like @dataclass
"-marker that can be used for all transformers from annotations to fields.
PEP 681 is available to earlier Python versions via typing_extensions
.
For a pure typing alternative, define your custom decorator to take a dataclass and modify it. A dataclass can be identified by its __dataclass_fields__
field.
from typing import Protocol, Any, TypeVar, Type, ClassVar
from dataclasses import Field
class DataClass(Protocol):
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
DC = TypeVar("DC", bound=DataClass)
def my_dataclass(klass: Type[DC]) -> Type[DC]:
...
This allows the type checker to understand and verify that a dataclass
class is needed.
@my_dataclass
@dataclass
class TestB:
a: int
b: str
TestB(0, "") # note: Revealed type is "so_test.TestB"
@my_dataclass
class TestC: # error: Value of type variable "DC" of "my_dataclass" cannot be "TestC"
a: int
b: str
The PEP 681 dataclass_transform
decorator is a marker for other decorators to show that they act "like" @dataclass
. In order to match the behaviour of @dataclass
, one has to use field_specifiers
to indicate that fields are denoted the same way.
from typing import dataclass_transform, TypeVar, Type
import dataclasses
T = TypeVar("T")
@dataclass_transform(
field_specifiers=(dataclasses.Field, dataclasses.field),
)
def my_dataclass(klass: Type[T]) -> Type[T]:
return something_else(dataclasses.dataclass(klass))
It is possible for the custom dataclass decorator to take all keywords as @dataclass
. dataclass_transform
can be used to mark their respective defaults, even when not accepted as keywords by the decorator itself.