Search code examples
pythonpython-typingmypy

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

  • 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, 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
    

    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.