# `typing.dataclass_transform`

```toml
[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

```py
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:

```py
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:

```py
from typing_extensions import dataclass_transform, Callable

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

## Types of dataclass-transformers

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

### Decorating a decorator function

```py
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

```py
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

```py
from typing_extensions import dataclass_transform

@dataclass_transform()
class ModelBase: ...

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

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:

```py
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

reveal_type(NormalOverwritten(1) < NormalOverwritten(2))  # revealed: bool

@order_default_false
class OrderFalse:
    inner: int

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

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

reveal_type(OrderFalseOverwritten(1) < OrderFalseOverwritten(2))  # revealed: bool

@order_default_true
class OrderTrue:
    inner: int

reveal_type(OrderTrue(1) < OrderTrue(2))  # revealed: bool

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

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

This also works for metaclass-based transformers:

```py
@dataclass_transform(order_default=True)
class OrderedModelMeta(type): ...

class OrderedModel(metaclass=OrderedModelMeta): ...

class TestWithMeta(OrderedModel):
    inner: int

reveal_type(TestWithMeta(1) < TestWithMeta(2))  # revealed: bool
```

And for base-class-based transformers:

```py
@dataclass_transform(order_default=True)
class OrderedModelBase: ...

class TestWithBase(OrderedModelBase):
    inner: int

reveal_type(TestWithBase(1) < TestWithBase(2))  # revealed: bool
```

### `kw_only_default`

When provided, sets the default value for the `kw_only` parameter of dataclass fields:

```py
from typing import dataclass_transform

@dataclass_transform(kw_only_default=True)
def create_model(*, kw_only: bool = True): ...

@create_model()
class Model1:
    name: str

reveal_type(Model1.__init__)  # revealed: (self: Model1, *, name: str) -> None

Model1(name="Harry")
# error: [missing-argument]
# error: [too-many-positional-arguments]
Model1("Harry")
```

This can be overridden by setting `kw_only=False` when applying the decorator:

```py
@create_model(kw_only=False)
class Model1KwOnlyFalse:
    name: str

reveal_type(Model1KwOnlyFalse.__init__)  # revealed: (self: Model1KwOnlyFalse, name: str) -> None

Model1KwOnlyFalse(name="Harry")
Model1KwOnlyFalse("Harry")
```

This also works for metaclass-based transformers:

```py
@dataclass_transform(kw_only_default=True)
class ModelMeta(type):
    def __new__(
        cls,
        name,
        bases,
        namespace,
        *,
        kw_only: bool = True,
    ): ...

class ModelBase(metaclass=ModelMeta): ...

class Model2(ModelBase):
    name: str

reveal_type(Model2.__init__)  # revealed: (self: Model2, *, name: str) -> None

Model2(name="Harry")
# error: [missing-argument]
# error: [too-many-positional-arguments]
Model2("Harry")

class Model2KwOnlyFalse(ModelBase, kw_only=False):
    name: str

reveal_type(Model2KwOnlyFalse.__init__)  # revealed: (self: Model2KwOnlyFalse, name: str) -> None

Model2KwOnlyFalse(name="Harry")
Model2KwOnlyFalse("Harry")
```

And for base-class-based transformers:

```py
@dataclass_transform(kw_only_default=True)
class ModelBase:
    def __init_subclass__(cls, kw_only: bool = False) -> None:
        pass

class Model3(ModelBase):
    name: str

reveal_type(Model3.__init__)  # revealed: (self: Model3, *, name: str) -> None

Model3(name="Harry")
# error: [missing-argument]
# error: [too-many-positional-arguments]
Model3("Harry")

class Model3KwOnlyFalse(ModelBase, kw_only=False):
    name: str

reveal_type(Model3KwOnlyFalse.__init__)  # revealed: (self: Model3KwOnlyFalse, name: str) -> None

Model3KwOnlyFalse(name="Harry")
Model3KwOnlyFalse("Harry")
```

### `frozen_default`

When provided, sets the default value for the `frozen` parameter of `field()`.

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
def create_model(*, frozen: bool = True): ...

@create_model()
class ImmutableModel:
    name: str

i = ImmutableModel(name="test")
i.name = "new"  # error: [invalid-assignment]
```

Again, this can be overridden by setting `frozen=False` when applying the decorator:

```py
@create_model(frozen=False)
class MutableModel:
    name: str

m = MutableModel(name="test")
m.name = "new"  # No error
```

This also works for metaclass-based transformers:

```py
@dataclass_transform(frozen_default=True)
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta): ...

class TestMeta(ModelBase):
    name: str

t = TestMeta(name="test")
t.name = "new"  # error: [invalid-assignment]
```

And for base-class-based transformers:

```py
@dataclass_transform(frozen_default=True)
class ModelBase: ...

class TestMeta(ModelBase):
    name: str

t = TestMeta(name="test")

t.name = "new"  # error: [invalid-assignment]
```

### Transformers using `**kwargs`

Dataclass transform parameters like `frozen` should be recognized even when the transformer doesn't
explicitly list them in its signature, but instead uses `**kwargs`.

#### Function-based transformer

```py
from typing import dataclass_transform, Callable

@dataclass_transform()
def create_model[T: type](**kwargs) -> Callable[[T], T]:
    raise NotImplementedError

@create_model(frozen=True)
class Frozen:
    name: str

f = Frozen("Alice")
f.name = "Bob"  # error: [invalid-assignment]
```

#### Metaclass-based transformer

```py
from typing import dataclass_transform

@dataclass_transform()
class ModelMeta(type):
    def __new__(cls, name, bases, namespace, **kwargs): ...

class ModelBase(metaclass=ModelMeta): ...

class Frozen(ModelBase, frozen=True):
    name: str

f = Frozen(name="test")
f.name = "new"  # error: [invalid-assignment]
```

#### Base-class-based transformer

```py
from typing import dataclass_transform

@dataclass_transform()
class ModelBase:
    def __init_subclass__(cls, **kwargs): ...

class Frozen(ModelBase, frozen=True):
    name: str

f = Frozen(name="test")
f.name = "new"  # error: [invalid-assignment]
```

### Combining parameters

Combining several of these parameters also works as expected:

```py
from typing import dataclass_transform

@dataclass_transform(eq_default=True, order_default=False, kw_only_default=True, frozen_default=True)
def create_model(*, eq: bool = True, order: bool = False, kw_only: bool = True, frozen: bool = True): ...

@create_model(eq=False, order=True, kw_only=False, frozen=False)
class OverridesAllParametersModel:
    name: str
    age: int

# Positional arguments are allowed:
model = OverridesAllParametersModel("test", 25)

# Mutation is allowed:
model.name = "new"  # No error

# Comparison methods are generated:
model < model  # No error
```

### Overwriting of default parameters on the dataclass-like class

In the following examples, we show how a model can overwrite the default parameters set by the
`dataclass_transform` decorator. In particular, we change from `frozen=True` to `frozen=False`, and
from `order=False` (default) to `order=True`:

#### Using function-based transformers

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
def default_frozen_model(*, frozen: bool = True, order: bool = False): ...

@default_frozen_model()
class Frozen:
    name: str

f = Frozen(name="test")
f.name = "new"  # error: [invalid-assignment]

Frozen(name="A") < Frozen(name="B")  # error: [unsupported-operator]

@default_frozen_model(frozen=False, order=True)
class Mutable:
    name: str

m = Mutable(name="test")
m.name = "new"  # No error

reveal_type(Mutable(name="A") < Mutable(name="B"))  # revealed: bool
```

#### Using metaclass-based transformers

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
class DefaultFrozenMeta(type):
    def __new__(
        cls,
        name,
        bases,
        namespace,
        *,
        frozen: bool = True,
        order: bool = False,
    ): ...

class DefaultFrozenModel(metaclass=DefaultFrozenMeta): ...

class Frozen(DefaultFrozenModel):
    name: str

f = Frozen(name="test")
f.name = "new"  # error: [invalid-assignment]

Frozen(name="A") < Frozen(name="B")  # error: [unsupported-operator]

class Mutable(DefaultFrozenModel, frozen=False, order=True):
    name: str

m = Mutable(name="test")
m.name = "new"  # No error

reveal_type(Mutable(name="A") < Mutable(name="B"))  # revealed: bool
```

#### Using base-class-based transformers

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
class DefaultFrozenModel:
    def __init_subclass__(
        cls,
        *,
        frozen: bool = True,
        order: bool = False,
    ): ...

class Frozen(DefaultFrozenModel):
    name: str

f = Frozen(name="test")
f.name = "new"  # error: [invalid-assignment]

Frozen(name="A") < Frozen(name="B")  # error: [unsupported-operator]

class Mutable(DefaultFrozenModel, frozen=False, order=True):
    name: str

m = Mutable(name="test")
m.name = "new"  # No error

reveal_type(Mutable(name="A") < Mutable(name="B"))  # revealed: bool
```

### Frozen inheritance

Just like for regular `@dataclass`es, mixing frozen and non-frozen `@dataclass_transform` classes in
an inheritance chain is not allowed. However, the root class of a `@dataclass_transform` hierarchy
(the class decorated with `@dataclass_transform()` or the class that directly specifies the
`@dataclass_transform` metaclass) is "neither frozen nor non-frozen", so both frozen and non-frozen
subclasses can inherit from it.

#### Using function-based transformers

For function-based transformers, all classes are either frozen or non-frozen. There is no special
root class.

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
def frozen_model(*, frozen: bool = True): ...

@frozen_model()
class FrozenParent:
    x: int

@frozen_model()
class FrozenChild(FrozenParent):
    y: int

@frozen_model(frozen=False)
# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `NonFrozenChild` cannot inherit from frozen dataclass `FrozenParent`"
class NonFrozenChild(FrozenParent):
    y: int

@frozen_model(frozen=False)
class NonFrozenParent:
    x: int

@frozen_model()
# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenFromNonFrozen` cannot inherit from non-frozen dataclass `NonFrozenParent`"
class FrozenFromNonFrozen(NonFrozenParent):
    y: int
```

#### Using metaclass-based transformers

For metaclass-based transformers, the class that is decorated with `@dataclass_transform` is the
root class that is "neither frozen nor non-frozen" (`DefaultFrozenMeta` in the example below). So
children of that class can be either frozen or non-frozen:

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
class FrozenMeta(type):
    def __new__(
        cls,
        name,
        bases,
        namespace,
        *,
        frozen: bool = True,
    ): ...

class DefaultFrozenModel(metaclass=FrozenMeta): ...
```

Both frozen and non-frozen classes can inherit from the root class:

```py
class FrozenParent(DefaultFrozenModel):
    x: int

class NonFrozenParent(DefaultFrozenModel, frozen=False):
    x: int
```

Inheriting from these classes is fine as long as we keep the frozen/non-frozen status consistent:

```py
class FrozenChild(FrozenParent):
    y: int

class NonFrozenChild(NonFrozenParent, frozen=False):
    y: int
```

But mixing frozen and non-frozen is not allowed at this level:

```py
# error: [invalid-frozen-dataclass-subclass]
class InvalidFrozenChild(NonFrozenParent, frozen=True):
    y: int

# error: [invalid-frozen-dataclass-subclass]
class InvalidNonFrozenChild(FrozenParent, frozen=False):
    y: int
```

#### Using base-class-based transformers

Similarly, for base-class-based transformers, the class that is decorated with
`@dataclass_transform` is the root class that is "neither frozen nor non-frozen"
(`DefaultFrozenModel` in the example below). So children of that class can be either frozen or
non-frozen:

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
class DefaultFrozenModel:
    def __init_subclass__(
        cls,
        *,
        frozen: bool = True,
    ): ...
```

Both frozen and non-frozen classes can inherit from that root model:

```py
class FrozenParent(DefaultFrozenModel):
    x: int

class NonFrozenParent(DefaultFrozenModel, frozen=False):
    x: int
```

Inheriting from these classes is fine as long as we keep the frozen/non-frozen status consistent:

```py
class FrozenChild(FrozenParent):
    y: int

class NonFrozenChild(NonFrozenParent, frozen=False):
    y: int
```

But mixing frozen and non-frozen is not allowed at this level:

```py
# error: [invalid-frozen-dataclass-subclass]
class InvalidFrozenChild(NonFrozenParent, frozen=True):
    y: int

# error: [invalid-frozen-dataclass-subclass]
class InvalidNonFrozenChild(FrozenParent, frozen=False):
    y: int
```

### Override diagnostics on dataclass-like classes

#### Frozen override diagnostics

If a frozen dataclass-like class defines `__setattr__` or `__delattr__`, a diagnostic is emitted:

```py
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
def frozen_model(*, frozen: bool = True): ...

@frozen_model()
class FrozenWithOverrides:
    x: int

    # error: [invalid-dataclass-override] "Cannot overwrite attribute `__setattr__` in frozen dataclass `FrozenWithOverrides`"
    def __setattr__(self, name: str, value: object) -> None: ...

    # error: [invalid-dataclass-override] "Cannot overwrite attribute `__delattr__` in frozen dataclass `FrozenWithOverrides`"
    def __delattr__(self, name: str) -> None: ...
```

No diagnostic is emitted if the class is not frozen:

```py
@frozen_model(frozen=False)
class NotFrozenWithOverrides:
    x: int

    def __setattr__(self, name: str, value: object) -> None: ...
    def __delattr__(self, name: str) -> None: ...
```

#### Order override diagnostics

If an ordered dataclass-like class defines comparison dunder methods, a diagnostic is emitted:

```py
from typing import dataclass_transform

@dataclass_transform()
def ordered_model(*, order: bool = False): ...

@ordered_model(order=True)
class OrderedWithOverrides:
    x: int

    # error: [invalid-dataclass-override] "Cannot overwrite attribute `__lt__` in dataclass `OrderedWithOverrides` with `order=True`"
    def __lt__(self, other: object) -> bool:
        return False
    # error: [invalid-dataclass-override] "Cannot overwrite attribute `__le__` in dataclass `OrderedWithOverrides` with `order=True`"
    def __le__(self, other: object) -> bool:
        return False
    # error: [invalid-dataclass-override] "Cannot overwrite attribute `__gt__` in dataclass `OrderedWithOverrides` with `order=True`"
    def __gt__(self, other: object) -> bool:
        return False
    # error: [invalid-dataclass-override] "Cannot overwrite attribute `__ge__` in dataclass `OrderedWithOverrides` with `order=True`"
    def __ge__(self, other: object) -> bool:
        return False
```

No diagnostic is emitted if the class does not use `order=True`:

```py
@ordered_model()
class NotOrderedWithOverrides:
    x: int

    def __lt__(self, other: object) -> bool:
        return False

    def __le__(self, other: object) -> bool:
        return False

    def __gt__(self, other: object) -> bool:
        return False

    def __ge__(self, other: object) -> bool:
        return False
```

## Other `dataclass` parameters

Other parameters from normal dataclasses can also be set on models created using
`dataclass_transform`.

### Using function-based transformers

```py
from typing_extensions import dataclass_transform, TypeVar, Callable

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

@dataclass_transform()
def fancy_model(*, slots: bool = False) -> Callable[[T], T]:
    raise NotImplementedError

@fancy_model()
class NoSlots:
    name: str

NoSlots.__slots__  # error: [unresolved-attribute]

@fancy_model(slots=True)
class WithSlots:
    name: str

reveal_type(WithSlots.__slots__)  # revealed: tuple[Literal["name"]]
```

### Using metaclass-based transformers

```py
from typing_extensions import dataclass_transform

@dataclass_transform()
class FancyMeta(type):
    def __new__(cls, name, bases, namespace, *, slots: bool = False):
        ...
        return super().__new__(cls, name, bases, namespace)

class FancyBase(metaclass=FancyMeta): ...

class NoSlots(FancyBase):
    name: str

# error: [unresolved-attribute]
NoSlots.__slots__

class WithSlots(FancyBase, slots=True):
    name: str

reveal_type(WithSlots.__slots__)  # revealed: tuple[Literal["name"]]
```

### Using base-class-based transformers

```py
from typing_extensions import dataclass_transform

@dataclass_transform()
class FancyBase:
    def __init_subclass__(cls, *, slots: bool = False):
        ...
        super().__init_subclass__()

class NoSlots(FancyBase):
    name: str

NoSlots.__slots__  # error: [unresolved-attribute]

class WithSlots(FancyBase, slots=True):
    name: str

reveal_type(WithSlots.__slots__)  # revealed: tuple[Literal["name"]]
```

## `field_specifiers`

The `field_specifiers` argument can be used to specify a list of functions that should be treated
similar to `dataclasses.field` for normal dataclasses.

The [`typing.dataclass_transform`] specification also allows classes (such as `dataclasses.Field`)
to be listed in `field_specifiers`, but it is currently unclear how this should work, and other type
checkers do not seem to support this either.

### For function-based transformers

```py
from typing_extensions import dataclass_transform, Any

def fancy_field(*, init: bool = True, kw_only: bool = False, alias: str | None = None) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
def fancy_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

@fancy_model
class Person:
    id: int = fancy_field(init=False)
    internal_name: str = fancy_field(alias="name")
    age: int | None = fancy_field(kw_only=True)

reveal_type(Person.__init__)  # revealed: (self: Person, name: str, *, age: int | None) -> None

alice = Person("Alice", age=30)

reveal_type(alice.id)  # revealed: int
reveal_type(alice.internal_name)  # revealed: str
reveal_type(alice.age)  # revealed: int | None
```

### For metaclass-based transformers

```py
from typing_extensions import dataclass_transform, Any

def fancy_field(*, init: bool = True, kw_only: bool = False, alias: str | None = None) -> Any: ...

@dataclass_transform(field_specifiers=(fancy_field,))
class FancyMeta(type):
    def __new__(cls, name, bases, namespace):
        ...
        return super().__new__(cls, name, bases, namespace)

class FancyBase(metaclass=FancyMeta): ...

class Person(FancyBase):
    id: int = fancy_field(init=False)
    internal_name: str = fancy_field(alias="name")
    age: int | None = fancy_field(kw_only=True)

reveal_type(Person.__init__)  # revealed: (self: Person, name: str, *, age: int | None) -> None

alice = Person("Alice", age=30)

reveal_type(alice.id)  # revealed: int
reveal_type(alice.internal_name)  # revealed: str
reveal_type(alice.age)  # revealed: int | None
```

### For base-class-based transformers

```py
from typing_extensions import dataclass_transform, Any

def fancy_field(*, init: bool = True, kw_only: bool = False, alias: str | None = None) -> Any: ...

@dataclass_transform(field_specifiers=(fancy_field,))
class FancyBase:
    def __init_subclass__(cls):
        ...
        super().__init_subclass__()

class Person(FancyBase):
    id: int = fancy_field(init=False)
    internal_name: str = fancy_field(alias="name")
    age: int | None = fancy_field(kw_only=True)

reveal_type(Person.__init__)  # revealed: (self: Person, name: str, *, age: int | None) -> None

alice = Person("Alice", age=30)

reveal_type(alice.id)  # revealed: int
reveal_type(alice.internal_name)  # revealed: str
reveal_type(alice.age)  # revealed: int | None
```

### With default arguments

Field specifiers can have default arguments that should be respected:

```py
from typing_extensions import dataclass_transform, Any

def fancy_field(*, init: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
def fancy_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

@fancy_model
class Person:
    id: int = fancy_field()
    name: str = fancy_field(init=True)

reveal_type(Person.__init__)  # revealed: (self: Person, name: str) -> None

Person(name="Alice")
```

### Field specifiers using `**kwargs`

Some field specifiers may use `**kwargs` to pass through standard parameters like `default`,
`default_factory`, `init`, `kw_only`, and `alias`. This section tests that all these parameters work
correctly when passed via `**kwargs` for all three kinds of transformers.

#### Function-based transformer

```py
from typing import Any
from typing_extensions import dataclass_transform

def field(**kwargs: Any) -> Any: ...
@dataclass_transform(field_specifiers=(field,))
def create_model[T](cls: type[T]) -> type[T]:
    return cls

@create_model
class Person:
    id: int = field(init=False)
    name: str
    age: int = field(default=0)
    tags: list[str] = field(default_factory=list)
    email: str = field(kw_only=True)
    internal_notes: str = field(alias="notes", default="")

# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str = ..., *, email: str) -> None
reveal_type(Person.__init__)

Person("Alice", 30, [], "some notes", email="alice@example.com")
Person("Bob", email="bob@example.com", notes="other notes")
```

#### Metaclass-based transformer

```py
from typing import Any
from typing_extensions import dataclass_transform

def field(**kwargs: Any) -> Any: ...

@dataclass_transform(field_specifiers=(field,))
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta): ...

class Person(ModelBase):
    id: int = field(init=False)
    name: str
    age: int = field(default=0)
    tags: list[str] = field(default_factory=list)
    email: str = field(kw_only=True)
    internal_notes: str = field(alias="notes", default="")

# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str = ..., *, email: str) -> None
reveal_type(Person.__init__)

Person("Alice", 30, [], "some notes", email="alice@example.com")
Person("Bob", email="bob@example.com", notes="other notes")
```

#### Base-class-based transformer

```py
from typing import Any
from typing_extensions import dataclass_transform

def field(**kwargs: Any) -> Any: ...

@dataclass_transform(field_specifiers=(field,))
class ModelBase: ...

class Person(ModelBase):
    id: int = field(init=False)
    name: str
    age: int = field(default=0)
    tags: list[str] = field(default_factory=list)
    email: str = field(kw_only=True)
    internal_notes: str = field(alias="notes", default="")

# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str = ..., *, email: str) -> None
reveal_type(Person.__init__)

Person("Alice", 30, [], "some notes", email="alice@example.com")
Person("Bob", email="bob@example.com", notes="other notes")
```

### Support for `alias`

The `alias` parameter in field specifiers allows providing an alternative name for the parameter in
the synthesized `__init__` method.

```py
from typing_extensions import dataclass_transform, Any

def field_with_alias(*, alias: str | None = None, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(field_with_alias,))
def model[T](cls: type[T]) -> type[T]:
    return cls

@model
class Person:
    internal_name: str = field_with_alias(alias="name")
    internal_age: int = field_with_alias(alias="age", kw_only=True)
```

The synthesized `__init__` method uses the alias names instead of the actual attribute names:

```py
reveal_type(Person.__init__)  # revealed: (self: Person, name: str, *, age: int) -> None
```

We can construct instances using the alias names:

```py
p = Person(name="Alice", age=30)
```

Passing the `name` parameter positionally also works:

```py
p = Person("Alice", age=30)
```

But the attributes are still accessed by their actual names:

```py
reveal_type(p.internal_name)  # revealed: str
reveal_type(p.internal_age)  # revealed: int
```

Trying to use the actual attribute names in the constructor results in an error:

```py
# error: [unknown-argument] "Argument `internal_age` does not match any known parameter"
# error: [missing-argument] "No argument provided for required parameter `age`"
p = Person(name="Alice", internal_age=30)
```

### With overloaded field specifiers

```py
from typing_extensions import dataclass_transform, overload, Any

@overload
def fancy_field(*, init: bool = True) -> Any: ...
@overload
def fancy_field(*, kw_only: bool = False) -> Any: ...
def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
def fancy_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

@fancy_model
class Person:
    id: int = fancy_field(init=False)
    name: str = fancy_field()
    age: int | None = fancy_field(kw_only=True)

reveal_type(Person.__init__)  # revealed: (self: Person, name: str, *, age: int | None) -> None
```

### Converter field specifier with overloaded callables

```py
from typing import Callable, TypeVar, overload
from typing_extensions import dataclass_transform

T = TypeVar("T")
S = TypeVar("S")

def model_field(*, converter: Callable[[S], T], default: S | None = None) -> T:
    raise NotImplementedError

@dataclass_transform(field_specifiers=(model_field,))
class ModelBase: ...

@overload
def overloaded_converter(s: str) -> int: ...
@overload
def overloaded_converter(s: list[str]) -> int: ...
def overloaded_converter(s: str | list[str], *args: str) -> int | str:
    return 0

class ConverterClass:
    @overload
    def __init__(self, val: str) -> None: ...
    @overload
    def __init__(self, val: bytes) -> None: ...
    def __init__(self, val: str | bytes) -> None:
        pass

class Model(ModelBase):
    field3: ConverterClass = model_field(converter=ConverterClass)
    field4: int = model_field(converter=overloaded_converter)
    # TODO: This should be accepted once overloaded class callables with richer signatures are
    # modeled in callable assignability.
    # error: [invalid-assignment]
    # error: [invalid-argument-type]
    field5: dict[str, str] = model_field(converter=dict, default=())
```

### Nested dataclass-transformers

Make sure that models are only affected by the field specifiers of their own transformer:

```py
from typing_extensions import dataclass_transform, Any
from dataclasses import field

def outer_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(outer_field,))
def outer_model[T](cls: type[T]) -> type[T]:
    # ...
    return cls

def inner_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(inner_field,))
def inner_model[T](cls: type[T]) -> type[T]:
    # ...
    return cls

@outer_model
class Outer:
    @inner_model
    class Inner:
        inner_a: int = inner_field(init=False)
        inner_b: str = outer_field(init=False)

    outer_a: int = outer_field(init=False)
    outer_b: str = inner_field(init=False)

reveal_type(Outer.__init__)  # revealed: (self: Outer, outer_b: str = ...) -> None
reveal_type(Outer.Inner.__init__)  # revealed: (self: Inner, inner_b: str = ...) -> None
```

## 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

```py
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

```py
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

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

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

### Field specifiers in nested class scopes

When a dataclass-like class is defined inside a method, field specifier calls must still be
recognized correctly. This is a regression test for a bug where standalone expression inference
(triggered by being inside a method scope) would lose the field specifier context.

```py
from typing_extensions import Any, dataclass_transform

def field(*, init: bool = True, kw_only: bool = False, default: Any = ...) -> Any: ...
@dataclass_transform(field_specifiers=(field,))
def create_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

class Outer:
    def method(self):
        @create_model
        class Model:
            x: int = field(kw_only=True)
            y: str

        reveal_type(Model.__init__)  # revealed: (self: Model, y: str, *, x: int) -> None
```

## Field ordering checks

Field ordering checks apply to classes created via `dataclass_transform`, just like normal
`dataclass`es.

### For function-based transformers

```py
from typing_extensions import Any, dataclass_transform

def field(*, init: bool = True, kw_only: bool = False, default: Any = ...) -> Any: ...
@dataclass_transform(field_specifiers=(field,))
def create_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

@create_model
class ValidModel:
    x: int
    y: str = "default"

@create_model
class ValidModelWithKWOnly:
    x: int = 1
    z: float = field(kw_only=True)

@create_model
class ValidModelWithInitFalse:
    x: int = field(init=False)
    y: str = "default"

@create_model
class InvalidModel:
    x: int = 1
    y: str  # error: [dataclass-field-order]

@dataclass_transform(field_specifiers=(field,), kw_only_default=True)
def create_kwonly_default_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

@create_kwonly_default_model
class ValidKWOnlyDefaultModel:
    x: int = 1
    y: str

@create_kwonly_default_model
class AlsoValidKWOnlyDefaultModel:
    x: int = 1
    y: str = field(kw_only=True)

@create_kwonly_default_model
class InvalidKWOnlyDefaultModel:
    x: int
    y: str = field(kw_only=False, default="default")
    z: bytes = field(kw_only=False)  # error: [dataclass-field-order]
```

### For metaclass-based transformers

```py
from typing_extensions import Any, dataclass_transform

def field(*, init: bool = True, kw_only: bool = False, default: Any = ...) -> Any: ...

@dataclass_transform(field_specifiers=(field,))
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta): ...

class ValidModel(ModelBase):
    x: int
    y: str = "default"

class ValidModelWithKWOnly(ModelBase):
    x: int = 1
    z: float = field(kw_only=True)

class ValidModelWithInitFalse(ModelBase):
    x: int = field(init=False)
    y: str = "default"

class InvalidModel(ModelBase):
    x: int = 1
    y: str  # error: [dataclass-field-order]

@dataclass_transform(field_specifiers=(field,), kw_only_default=True)
class KWOnlyDefaultModelMeta(type): ...

class KWOnlyDefaultModelBase(metaclass=KWOnlyDefaultModelMeta): ...

class ValidKWOnlyDefaultModel(KWOnlyDefaultModelBase):
    x: int = 1
    y: str

class AlsoValidKWOnlyDefaultModel(KWOnlyDefaultModelBase):
    x: int = 1
    y: str = field(kw_only=True)

class InvalidKWOnlyDefaultModel(KWOnlyDefaultModelBase):
    x: int
    y: str = field(kw_only=False, default="default")
    z: bytes = field(kw_only=False)  # error: [dataclass-field-order]
```

### For base-class-based transformers

```py
from typing_extensions import Any, dataclass_transform

def field(*, init: bool = True, kw_only: bool = False, default: Any = ...) -> Any: ...

@dataclass_transform(field_specifiers=(field,))
class ModelBase:
    def __init_subclass__(cls):
        ...
        super().__init_subclass__()

class ValidModel(ModelBase):
    x: int
    y: str = "default"

class ValidModelWithKWOnly(ModelBase):
    x: int = 1
    z: float = field(kw_only=True)

class ValidModelWithInitFalse(ModelBase):
    x: int = field(init=False)
    y: str = "default"

class InvalidModel(ModelBase):
    x: int = 1
    y: str  # error: [dataclass-field-order]

@dataclass_transform(field_specifiers=(field,), kw_only_default=True)
class KWOnlyDefaultModelBase:
    def __init_subclass__(cls):
        ...
        super().__init_subclass__()

class ValidKWOnlyDefaultModel(KWOnlyDefaultModelBase):
    x: int = 1
    y: str

class AlsoValidKWOnlyDefaultModel(KWOnlyDefaultModelBase):
    x: int = 1
    y: str = field(kw_only=True)

class InvalidKWOnlyDefaultModel(KWOnlyDefaultModelBase):
    x: int
    y: str = field(kw_only=False, default="default")
    z: bytes = field(kw_only=False)  # error: [dataclass-field-order]
```

### Use cases

#### Home Assistant

Home Assistant uses a pattern like this, where a `@dataclass`-decorated class inherits from a base
class that is itself a `dataclass`-like construct via a metaclass-based dataclass transformer. Make
sure that we recognize all fields in a hierarchy like this:

```py
from dataclasses import dataclass
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
class ModelMeta(type):
    pass

class Sensor(metaclass=ModelMeta):
    key: int

@dataclass(frozen=True, kw_only=True)
class TemperatureSensor(Sensor):
    name: str

t = TemperatureSensor(key=1, name="Temperature Sensor")
reveal_type(t.key)  # revealed: int
reveal_type(t.name)  # revealed: str
```

## `__dataclass_fields__` and `DataclassInstance` protocol

Classes created via `dataclass_transform` should have `__dataclass_fields__` and
`__dataclass_params__` attributes, allowing them to satisfy the `DataclassInstance` protocol. This
enables use of `dataclasses.fields`, `dataclasses.asdict`, `dataclasses.replace`, etc.

### Function-based transformer

```py
from dataclasses import fields, asdict, replace, Field
from typing import dataclass_transform, Any

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

@create_model
class Person:
    name: str
    age: int

p = Person("Alice", 30)

reveal_type(Person.__dataclass_fields__)  # revealed: dict[str, Field[Any]]
reveal_type(p.__dataclass_fields__)  # revealed: dict[str, Field[Any]]

reveal_type(fields(Person))  # revealed: tuple[Field[Any], ...]
reveal_type(asdict(p))  # revealed: dict[str, Any]
reveal_type(replace(p, name="Bob"))  # revealed: Person
```

### Metaclass-based transformer

```py
from dataclasses import fields, asdict, replace, Field
from typing import dataclass_transform, Any

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

class ModelBase(metaclass=ModelMeta): ...

class Person(ModelBase):
    name: str
    age: int

p = Person("Alice", 30)

reveal_type(Person.__dataclass_fields__)  # revealed: dict[str, Field[Any]]
reveal_type(p.__dataclass_fields__)  # revealed: dict[str, Field[Any]]

reveal_type(fields(Person))  # revealed: tuple[Field[Any], ...]
reveal_type(asdict(p))  # revealed: dict[str, Any]
reveal_type(replace(p, name="Bob"))  # revealed: Person
```

### Base-class-based transformer

```py
from dataclasses import fields, asdict, replace, Field
from typing import dataclass_transform, Any

@dataclass_transform()
class ModelBase: ...

class Person(ModelBase):
    name: str
    age: int

p = Person("Alice", 30)

reveal_type(Person.__dataclass_fields__)  # revealed: dict[str, Field[Any]]
reveal_type(p.__dataclass_fields__)  # revealed: dict[str, Field[Any]]

reveal_type(fields(Person))  # revealed: tuple[Field[Any], ...]
reveal_type(asdict(p))  # revealed: dict[str, Any]
reveal_type(replace(p, name="Bob"))  # revealed: Person
```

## Calling decorator function directly with a class argument

When a function decorated with `@dataclass_transform()` is called directly with a class argument
(not used as a decorator), it should return the class with the dataclass transformation applied.

### Basic case

```py
from typing_extensions import dataclass_transform

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

class A:
    x: int

B = my_dataclass(A)

reveal_type(B)  # revealed: <class 'A'>

B(1)
```

### Function with additional parameters

```py
from typing_extensions import dataclass_transform

@dataclass_transform()
def my_dataclass[T](cls: type[T], *, order: bool = False) -> type[T]:
    return cls

class A:
    x: int

B = my_dataclass(A, order=True)

reveal_type(B)  # revealed: <class 'A'>

reveal_type(B(1) < B(2))  # revealed: bool
```

### Overloaded decorator function

When the decorator function has overloads (one for direct class application, one for returning a
decorator), calling it with a class should return the class type.

```py
from typing_extensions import dataclass_transform, Callable, overload

@overload
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]: ...
@overload
def my_dataclass[T]() -> Callable[[type[T]], type[T]]: ...
def my_dataclass[T](cls: type[T] | None = None) -> type[T] | Callable[[type[T]], type[T]]:
    raise NotImplementedError

class A:
    x: int

B = my_dataclass(A)

reveal_type(B)  # revealed: <class 'A'>

B(1)
```

### Passing a specialized generic class

When calling a `@dataclass_transform()` decorated function with a specialized generic class, the
specialization should be preserved.

```py
from typing_extensions import dataclass_transform

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

class A[T]:
    x: T

B = my_dataclass(A[int])

reveal_type(B)  # revealed: <class 'A[int]'>

B(1)
```

### Decorator factory with class parameter

When a `@dataclass_transform()` decorated function takes a class as a parameter but is used as a
decorator factory (returns a decorator), the dataclass behavior should be applied to the decorated
class, not to the parameter class.

```py
from typing_extensions import dataclass_transform

@dataclass_transform()
def hydrated_dataclass[T](target: type[T], *, frozen: bool = False):
    def decorator[U](cls: type[U]) -> type[U]:
        return cls
    return decorator

class Target:
    pass

decorator = hydrated_dataclass(Target)
reveal_type(decorator)  # revealed: <decorator produced by dataclass-like function>

@hydrated_dataclass(Target)
class Model:
    x: int

# Model should be a dataclass-like class with x as a field
Model(x=1)
reveal_type(Model.__init__)  # revealed: (self: Model, x: int) -> None
```

## `__dataclass_transform__` compatibility

For backwards compatibility with pre-3.11 Python, ty recognizes any function named
`__dataclass_transform__` as equivalent to `typing.dataclass_transform`, regardless of which module
it is defined in. This matches [pyright's behavior].

```py
from typing import TypeVar, Callable, Any

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

def __dataclass_transform__(
    *,
    eq_default: bool = True,
    order_default: bool = False,
    kw_only_default: bool = False,
    frozen_default: bool = False,
    field_specifiers: tuple[type | Callable[..., Any], ...] = (),
    **kwargs: Any,
) -> Callable[[T], T]:
    def decorator(cls: T) -> T:
        return cls
    return decorator

@__dataclass_transform__(kw_only_default=True)
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta): ...

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

reveal_type(User.__init__)  # revealed: (self: User, *, id: int, name: str) -> None

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

# error: [missing-argument]
User()
```

## Legacy `field_descriptors` compatibility

Earlier versions of the dataclass-transform spec used the parameter name `field_descriptors` instead
of `field_specifiers`. We should still recognize this shape for compatibility.

```py
from typing import Any, Callable, TypeVar

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

def __dataclass_transform__(
    *,
    eq_default: bool = True,
    order_default: bool = False,
    kw_only_default: bool = False,
    field_descriptors: tuple[type | Callable[..., Any], ...] = (),
) -> Callable[[T], T]:
    def decorator(cls: T) -> T:
        return cls
    return decorator

class FieldInfo:
    def __init__(self, default: Any = None, **kwargs: Any) -> None: ...

def Field(default: Any = None, **kwargs: Any) -> Any:
    return FieldInfo(default=default, **kwargs)

@__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo))
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta):
    def __init__(self, **data: Any) -> None: ...

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

reveal_type(User.__init__)  # revealed: (self: User, *, id: int, name: str) -> None
User(id=1, name="Test")

# error: [missing-argument]
User()
```

## Converters

```toml
[environment]
python-version = "3.13"  # we need __replace__ below
```

When a field specifier uses a `converter` parameter, the synthesized `__init__` parameter type
should be the converter's input type (i.e. the type of its first positional parameter), not the
field's declared type.

```py
from typing_extensions import Any, Callable, dataclass_transform

def field[T, R](*, converter: Callable[[T], R] | None = None, default: T | None = None) -> R:
    raise NotImplementedError

@dataclass_transform(field_specifiers=(field,))
def my_model[T](cls: type[T]) -> type[T]:
    return cls

def str_to_int(x: str) -> int:
    return int(x)

@my_model
class Basic:
    a: int = field(converter=str_to_int)
    b: int = field(converter=str_to_int, default="0")

reveal_type(Basic.__init__)  # revealed: (self: Basic, a: str, b: str = ...) -> None

Basic("1", "2")
Basic("1")
Basic(a="1", b="2")
Basic(a="1")

Basic("1", 2)  # error: [invalid-argument-type]
Basic(1, "2")  # error: [invalid-argument-type]
```

Reading an attribute returns the field's declared type, but writes to an attribute are verified
against the converter's input type:

```py
basic = Basic("1", "2")
reveal_type(basic.a)  # revealed: int
reveal_type(basic.b)  # revealed: int

basic.a = "2"
basic.a = 3  # error: [invalid-assignment]

basic.b = "1"
basic.b = 1  # error: [invalid-assignment]
```

This also works when the object type is a type alias to a dataclass instance:

```py
type BasicAlias = Basic

def _(obj: BasicAlias):
    reveal_type(obj.a)  # revealed: int
    obj.a = "2"
    obj.a = 3  # error: [invalid-assignment]
```

The default parameter for a converter field should also be verified against the converter's input
type:

```py
@my_model
class WrongDefault:
    # Note: It is plausible that changes to the generics solver could make this error message better, but it's
    # hard to come up with a good heuristic on what the rule should be. From looking at the code, it looks like
    # the type of the `default` parameter is just wrong, but the error message wants you to change the converter
    # function instead. And in principle, that is just as likely to be the issue. But especially since we expand
    # `T` to a `str | Literal[0]` union here, this is a bit confusing:
    # error: [invalid-argument-type] "Argument to function `field` is incorrect: Expected `((str | Literal[0], /) -> int) | None`, found `def str_to_int(x: str) -> int`"
    a: int = field(converter=str_to_int, default=0)
```

When a field specifier declares a default value for `converter`, fields that don't explicitly pass a
converter will use the default:

```py
from typing_extensions import Any

# TODO: no error here (https://github.com/astral-sh/ty/issues/592)
# error: [invalid-parameter-default]
def field_with_default_converter[T, R](*, converter: Callable[[T], R] = str_to_int, default: T | None = None) -> R:
    raise NotImplementedError

@dataclass_transform(field_specifiers=(field_with_default_converter,))
def model_with_default_converter[T](cls: type[T]) -> type[T]:
    return cls

@model_with_default_converter
class WithDefaultConverter:
    with_default_converter: int = field_with_default_converter()
    overwritten_converter: str = field_with_default_converter(converter=str)

# revealed: (self: WithDefaultConverter, with_default_converter: str, overwritten_converter: object) -> None
reveal_type(WithDefaultConverter.__init__)
```

Classes can also be used as converters:

```py
class PermissiveNumber:
    def __init__(self, x: int | str):
        self.value = int(x)

@my_model
class WithClassConverter:
    a: PermissiveNumber = field(converter=PermissiveNumber)
    b: float = field(converter=float)

# revealed: (self: WithClassConverter, a: int | str, b: str | Buffer | SupportsFloat | SupportsIndex) -> None
reveal_type(WithClassConverter.__init__)

WithClassConverter(1, 2.5)
WithClassConverter("1", "2.5")

with_class_converter = WithClassConverter("1", "2.5")
reveal_type(with_class_converter.a)  # revealed: PermissiveNumber
reveal_type(with_class_converter.b)  # revealed: int | float

with_class_converter.a = "2"
with_class_converter.a = 1.5  # error: [invalid-assignment]

with_class_converter.b = "3.5"
with_class_converter.b = None  # error: [invalid-assignment]
```

Generic classes and generic functions can also be used as converters:

```py
def duplicate[T](x: T) -> tuple[T, T]:
    return (x, x)

@my_model
class WithGenericClassConverter:
    a: list[str] = field(converter=list)
    b: tuple[int, int] = field(converter=duplicate)

# TODO: The input types should ideally be `a: Iterable[str]` and `b: int` here
# revealed: (self: WithGenericClassConverter, a: Iterable[Unknown], b: Unknown) -> None
reveal_type(WithGenericClassConverter.__init__)

WithGenericClassConverter(("a", "b", "c"), 1)

# TODO: these should ideally be errors
WithGenericClassConverter((1, 2, 3), 1)
WithGenericClassConverter(("a", "b", "c"), "foo")
```

When a converter function is overloaded, the input type is the union of all first parameter types:

```py
from typing import overload

@overload
def serialize(x: str, /) -> bytes: ...
@overload
def serialize(x: list[str], /) -> bytes: ...
def serialize(x: str | list[str], /) -> bytes:
    raise NotImplementedError

@my_model
class WithOverloadedConverter:
    data: bytes = field(converter=serialize)

reveal_type(WithOverloadedConverter.__init__)  # revealed: (self: WithOverloadedConverter, data: str | list[str]) -> None

WithOverloadedConverter("string")
WithOverloadedConverter(["a", "b", "c"])

WithOverloadedConverter(123)  # error: [invalid-argument-type]

with_overloaded_converter = WithOverloadedConverter("string")
reveal_type(with_overloaded_converter.data)  # revealed: bytes

with_overloaded_converter.data = ["new", "data"]

with_overloaded_converter.data = 123  # error: [invalid-assignment]
```

A variadic converter (`*args`) uses the variadic parameter's element type as the input type:

```py
def variadic_converter(*args: str) -> int:
    return int(args[0])

@my_model
class WithVariadicConverter:
    x: int = field(converter=variadic_converter)

reveal_type(WithVariadicConverter.__init__)  # revealed: (self: WithVariadicConverter, x: str) -> None

WithVariadicConverter("1")
WithVariadicConverter(1)  # error: [invalid-argument-type]
```

When the declared field type does not match the converter's output type, we emit a diagnostic:

```py
@my_model
class WrongConverterOutput:
    # error: [invalid-assignment] "Object of type `dataclasses.Field[int]` is not assignable to `bytes`"
    x: bytes = field(converter=str_to_int)
```

This also works for overloaded converters with multiple output types:

```py
@overload
def validate(x: str) -> str: ...
@overload
def validate(x: int) -> int: ...
def validate(x: str | int) -> str | int:
    return x

@my_model
class WrongOverloadedConverterOutput:
    correct: str | int = field(converter=validate)

    # error: [invalid-assignment] "Object of type `dataclasses.Field[str | int]` is not assignable to `int`"
    incorrect1: int = field(converter=validate)
    # error: [invalid-assignment] "Object of type `dataclasses.Field[str | int]` is not assignable to `str`"
    incorrect2: str = field(converter=validate)
```

When a field-specifier call contains both a default value and a converter, the default value should
be verified against the converter's input type, not the field's declared type:

```py
@my_model
class ConverterWithDefault:
    correct: int = field(converter=str_to_int, default="0")

    # error: [invalid-argument-type]
    incorrect1: int = field(converter=str_to_int, default=0)

    # TODO: this should be an error
    incorrect2: int = field(default="0")
```

We currently assume that the presence of a converter also implies that the this converter function
will be called when using `replace` (this is how `attrs` behaves):

```py
basic = Basic("1", "2")

# __replace__ uses the converter input type (str):
reveal_type(Basic.__replace__)  # revealed: (self: Basic, *, a: str = ..., b: str = ...) -> Basic
```

Converter fields inherited from a base class should also be validated correctly:

```py
@my_model
class Base:
    a: int = field(converter=str_to_int)

@my_model
class Child(Base):
    b: int = field(converter=str_to_int)

child = Child("1", "2")
reveal_type(child.a)  # revealed: int
reveal_type(child.b)  # revealed: int

child.a = "2"
child.a = 3  # error: [invalid-assignment]

child.b = "2"
child.b = 3  # error: [invalid-assignment]
```

We also validate writes to unions of converter fields:

```py
def str_or_bytes_to_int(x: str | bytes) -> int:
    raise NotImplementedError

@my_model
class ModelA:
    x: int = field(converter=str_to_int)

@my_model
class ModelB:
    x: int = field(converter=str_or_bytes_to_int)

def _(m: ModelA | ModelB):
    # Allowed, since both converters accept str
    m.x = "1"

    m.x = b"1"  # error: [invalid-assignment]
```

[pyright's behavior]: https://github.com/microsoft/pyright/blob/1.1.396/packages/pyright-internal/src/analyzer/dataClasses.ts#L1024-L1033
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
