Calling a `dataclass_transform` function with a `type[...]` argument returns an internal placeholder type, not the declared return type
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
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 builtPoint 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 diagnosticsA 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:
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:
@dataclass_transform(frozen_default=True)
def event[E](cls: type[E]) -> type[E]:
built = dataclass(frozen=True, slots=True)(cls)
EVENTS.add(built)
return builtPossibly related to the function-decorator item in #1327.
Environment
ty 0.0.81 (4edd7724a 2026-09-14), Python 3.15.0rc2, Windows 11.
Source: astral-sh/ty