'How to annotate that a function produces a dataclass?
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)
Solution 1:[1]
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
.
Enforcing dataclasses
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
import dataclasses
class DataClass(Protocol):
__dataclass_fields__: dict[str, 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
Custom dataclass-like decorators
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.
Solution 2:[2]
The problem is that mypy understands the metaclass decorator
and its magic
about __init__
, but does not understand the dataclass function
:
@dataclass
class Test:
a: int
# Mypy: Revealed type is "def (self: Test, a: builtins.int)"
reveal_type(Test.__init__)
class Test2:
a: int
#Mypy: Revealed type is "def (self: builtins.object)"
reveal_type(dataclass(Test2).__init__)
As you can see, the __init__
method of Test2
does not accept any arguments.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | hussic |