ruff/crates/ty_python_semantic/resources/mdtest/dataclass_transform.md
2025-05-03 19:49:15 +02:00

5.7 KiB

typing.dataclass_transform

[environment]
python-version = "3.12"

dataclass_transform is a decorator that can be used to let type checkers know that a function, class, or metaclass is a dataclass-like construct.

Basic example

from typing_extensions import dataclass_transform

@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
    # modify cls
    return cls

@my_dataclass
class Person:
    name: str
    age: int | None = None

Person("Alice", 20)
Person("Bob", None)
Person("Bob")

# error: [missing-argument]
Person()

Decorating decorators that take parameters themselves

If we want our dataclass-like decorator to also take parameters, that is also possible:

from typing_extensions import dataclass_transform, Callable

@dataclass_transform()
def versioned_class[T](*, version: int = 1):
    def decorator(cls):
        # modify cls
        return cls
    return decorator

@versioned_class(version=2)
class Person:
    name: str
    age: int | None = None

Person("Alice", 20)

# error: [missing-argument]
Person()

We properly type-check the arguments to the decorator:

from typing_extensions import dataclass_transform, Callable

# error: [invalid-argument-type]
@versioned_class(version="a string")
class C:
    name: str

Types of decorators

The examples from this section are straight from the Python documentation on typing.dataclass_transform.

Decorating a decorator function

from typing_extensions import dataclass_transform

@dataclass_transform()
def create_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

@create_model
class CustomerModel:
    id: int
    name: str

CustomerModel(id=1, name="Test")

Decorating a metaclass

from typing_extensions import dataclass_transform

@dataclass_transform()
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta): ...

class CustomerModel(ModelBase):
    id: int
    name: str

CustomerModel(id=1, name="Test")

# error: [missing-argument]
CustomerModel()

Decorating a base class

from typing_extensions import dataclass_transform

@dataclass_transform()
class ModelBase: ...

class CustomerModel(ModelBase):
    id: int
    name: str

# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")

Arguments to dataclass_transform

eq_default

eq=True/False does not have a observable effect (apart from a minor change regarding whether other is positional-only or not, which is not modelled at the moment).

order_default

The order_default argument controls whether methods such as __lt__ are generated by default. This can be overwritten using the order argument to the custom decorator:

from typing_extensions import dataclass_transform

@dataclass_transform()
def normal(*, order: bool = False):
    raise NotImplementedError

@dataclass_transform(order_default=False)
def order_default_false(*, order: bool = False):
    raise NotImplementedError

@dataclass_transform(order_default=True)
def order_default_true(*, order: bool = True):
    raise NotImplementedError

@normal
class Normal:
    inner: int

Normal(1) < Normal(2)  # error: [unsupported-operator]

@normal(order=True)
class NormalOverwritten:
    inner: int

NormalOverwritten(1) < NormalOverwritten(2)

@order_default_false
class OrderFalse:
    inner: int

OrderFalse(1) < OrderFalse(2)  # error: [unsupported-operator]

@order_default_false(order=True)
class OrderFalseOverwritten:
    inner: int

OrderFalseOverwritten(1) < OrderFalseOverwritten(2)

@order_default_true
class OrderTrue:
    inner: int

OrderTrue(1) < OrderTrue(2)

@order_default_true(order=False)
class OrderTrueOverwritten:
    inner: int

# error: [unsupported-operator]
OrderTrueOverwritten(1) < OrderTrueOverwritten(2)

kw_only_default

To do

field_specifiers

To do

Overloaded dataclass-like decorators

In the case of an overloaded decorator, the dataclass_transform decorator can be applied to the implementation, or to one of the overloads.

Applying dataclass_transform to the implementation

from typing_extensions import dataclass_transform, TypeVar, Callable, overload

T = TypeVar("T", bound=type)

@overload
def versioned_class(
    cls: T,
    *,
    version: int = 1,
) -> T: ...
@overload
def versioned_class(
    *,
    version: int = 1,
) -> Callable[[T], T]: ...
@dataclass_transform()
def versioned_class(
    cls: T | None = None,
    *,
    version: int = 1,
) -> T | Callable[[T], T]:
    raise NotImplementedError

@versioned_class
class D1:
    x: str

@versioned_class(version=2)
class D2:
    x: str

D1("a")
D2("a")

D1(1.2)  # error: [invalid-argument-type]
D2(1.2)  # error: [invalid-argument-type]

Applying dataclass_transform to an overload

from typing_extensions import dataclass_transform, TypeVar, Callable, overload

T = TypeVar("T", bound=type)

@overload
@dataclass_transform()
def versioned_class(
    cls: T,
    *,
    version: int = 1,
) -> T: ...
@overload
def versioned_class(
    *,
    version: int = 1,
) -> Callable[[T], T]: ...
def versioned_class(
    cls: T | None = None,
    *,
    version: int = 1,
) -> T | Callable[[T], T]:
    raise NotImplementedError

@versioned_class
class D1:
    x: str

@versioned_class(version=2)
class D2:
    x: str

# TODO: these should not be errors
D1("a")  # error: [too-many-positional-arguments]
D2("a")  # error: [too-many-positional-arguments]

# TODO: these should be invalid-argument-type errors
D1(1.2)  # error: [too-many-positional-arguments]
D2(1.2)  # error: [too-many-positional-arguments]