#4546·ty

Calling a `dataclass_transform` function with a `type[...]` argument returns an internal placeholder type, not the declared return type

Author: BruceEckelCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbugdataclasses

Summary

A function decorated with @dataclass_transform and declared (cls: type[T]) -> type[T] returns the right type when called with a class literal. Called with an argument whose type is type[...] (type[E] for a type variable, type[SomeClass], or bare type), the call is typed as <decorator produced by dataclass-like function> instead of the declared type[T]. The result then fails wherever a type is required, including as the return value of a function whose signature matches the callee's.

The same signature without @dataclass_transform is inferred correctly, so the marker appears to switch the call onto a decorator-specific path that handles only class literals.

Decorator syntax is unaffected: @record above a class gives the class its synthesized __init__() and frozen fields as expected. Only the call form goes wrong, and only when the argument is not a class literal. A decorator is called directly whenever one class decorator builds on another, as in the last section below.

Reproduction

python
from dataclasses import dataclass
from typing import dataclass_transform, reveal_type


@dataclass_transform(frozen_default=True)
def record[T](cls: type[T]) -> type[T]:
    return dataclass(frozen=True, slots=True)(cls)


@record
class Decorated:  # Baseline: decorator syntax works
    x: int


reveal_type(Decorated)     # <class 'Decorated'>: correct
reveal_type(Decorated(1))  # Decorated: correct


class Point:
    x: int


def literal() -> None:
    reveal_type(record(Point))  # <class 'Point'>: correct


def annotated(cls: type[Point]) -> None:
    reveal_type(record(cls))  # placeholder; expected type[Point]


def generic[E](cls: type[E]) -> type[E]:
    built = record(cls)
    reveal_type(built)  # placeholder; expected type[E]
    return built  # error[invalid-return-type]


def undecorated[T](cls: type[T]) -> type[T]:
    return cls


def control[E](cls: type[E]) -> type[E]:
    built = undecorated(cls)
    reveal_type(built)  # type[E@control]: correct
    return built

Point is left undecorated on purpose: it is the argument to the direct calls.

Output, ty 0.0.81 (4edd7724a 2026-09-14)

info[revealed-type]: Revealed type
  --> repro.py:15:13
   |
15 | reveal_type(Decorated)     # <class 'Decorated'>: correct
   |             ^^^^^^^^^ `<class 'Decorated'>`

info[revealed-type]: Revealed type
  --> repro.py:16:13
   |
16 | reveal_type(Decorated(1))  # Decorated: correct
   |             ^^^^^^^^^^^^ `Decorated`

info[revealed-type]: Revealed type
  --> repro.py:24:17
   |
24 |     reveal_type(record(Point))  # <class 'Point'>: correct
   |                 ^^^^^^^^^^^^^ `<class 'Point'>`

info[revealed-type]: Revealed type
  --> repro.py:28:17
   |
28 |     reveal_type(record(cls))  # placeholder; expected type[Point]
   |                 ^^^^^^^^^^^ `<decorator produced by dataclass-like function>`

info[revealed-type]: Revealed type
  --> repro.py:33:17
   |
33 |     reveal_type(built)  # placeholder; expected type[E]
   |                 ^^^^^ `<decorator produced by dataclass-like function>`

error[invalid-return-type]: Return type does not match returned value
  --> repro.py:34:12
   |
31 | def generic[E](cls: type[E]) -> type[E]:
   |                                 ------- Expected `type[E@generic]` because of return type
32 |     built = record(cls)
33 |     reveal_type(built)  # placeholder; expected type[E]
34 |     return built  # error[invalid-return-type]
   |            ^^^^^ expected `type[E@generic]`, found `<decorator produced by dataclass-like function>`

info[revealed-type]: Revealed type
  --> repro.py:43:17
   |
43 |     reveal_type(built)  # type[E@control]: correct
   |                 ^^^^^ `type[E@control]`

Found 7 diagnostics

A parameter annotated bare type gives the same placeholder as type[Point].

Expected

record(cls) has the declared return type, type[Point] in annotated() and type[E@generic] in generic(), and generic() checks clean.

Pyright 1.1.414 on the same file reports 0 errors and reveals:

repro.py:15:13 - information: Type of "Decorated" is "type[Decorated]"
repro.py:16:13 - information: Type of "Decorated(1)" is "Decorated"
repro.py:24:17 - information: Type of "record(Point)" is "type[Point]"
repro.py:28:17 - information: Type of "record(cls)" is "type[Point]"
repro.py:33:17 - information: Type of "built" is "type[E@generic]"
repro.py:43:17 - information: Type of "built" is "type[E@control]"

Where it comes up

A class decorator that both registers a class and makes it a slotted data class has to register the object dataclass() returns, since slots=True creates a new class. Building that class with the project's own transform is the natural way to write it:

python
EVENTS: set[type] = set()


@dataclass_transform(frozen_default=True)
def event[E](cls: type[E]) -> type[E]:
    built = record(cls)   # reuse the project's own transform
    EVENTS.add(built)     # error[invalid-argument-type]
    return built          # error[invalid-return-type]

ty reports, for those two lines:

error[invalid-argument-type]: Argument to bound method `set.add` is incorrect
   Expected `type`, found `<decorator produced by dataclass-like function>`
error[invalid-return-type]: Return type does not match returned value
   expected `type[E@event]`, found `<decorator produced by dataclass-like function>`

The workaround is to call dataclass() directly in place of record(cls), which checks clean:

python
@dataclass_transform(frozen_default=True)
def event[E](cls: type[E]) -> type[E]:
    built = dataclass(frozen=True, slots=True)(cls)
    EVENTS.add(built)
    return built

Possibly related to the function-decorator item in #1327.

Environment

ty 0.0.81 (4edd7724a 2026-09-14), Python 3.15.0rc2, Windows 11.