crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
Decorating a class with @dataclass is a convenient way to add special methods such as __init__,
__repr__, and __eq__ to a class. The following example shows the basic usage of the @dataclass
decorator. By default, only the three mentioned methods are generated.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int | None = None
alice1 = Person("Alice", 30)
alice2 = Person(name="Alice", age=30)
alice3 = Person(age=30, name="Alice")
alice4 = Person("Alice", age=30)
reveal_type(alice1) # revealed: Person
reveal_type(type(alice1)) # revealed: type[Person]
reveal_type(alice1.name) # revealed: str
reveal_type(alice1.age) # revealed: int | None
reveal_type(repr(alice1)) # revealed: str
reveal_type(alice1 == alice2) # revealed: bool
reveal_type(alice1 == "Alice") # revealed: Literal[False]
bob = Person("Bob")
bob2 = Person("Bob", None)
bob3 = Person(name="Bob")
bob4 = Person(name="Bob", age=None)
The signature of the __init__ method is generated based on the classes attributes. The following
calls are not valid:
# error: [missing-argument]
Person()
# error: [too-many-positional-arguments]
Person("Eve", 20, "too many arguments")
# error: [invalid-argument-type]
Person("Eve", "string instead of int")
# error: [invalid-argument-type]
# error: [invalid-argument-type]
Person(20, "Eve")
__init__Declarations in the class body are used to generate the signature of the __init__ method. If the
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
the default value.
from dataclasses import dataclass
@dataclass
class D:
x: int
y: str = "default"
z: int | None = 1 + 2
reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = "default", z: int | None = 3) -> None
This also works if the declaration and binding are split:
@dataclass
class D:
x: int | None
x = None
reveal_type(D.__init__) # revealed: (self: D, x: int | None = None) -> None
Non-fully static types are handled correctly:
from typing import Any
@dataclass
class C:
w: type[Any]
x: Any
y: int | Any
z: tuple[int, Any]
reveal_type(C.__init__) # revealed: (self: C, w: type[Any], x: Any, y: int | Any, z: tuple[int, Any]) -> None
Variables without annotations are ignored:
@dataclass
class D:
x: int
y = 1
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
If attributes without default values are declared after attributes with default values, a
TypeError will be raised at runtime. We emit a diagnostic in that case:
from dataclasses import field
@dataclass
class D:
x: int = 1
# error: [dataclass-field-order] "Required field `y` cannot be defined after fields with default values"
y: str
@dataclass
class E:
x: int = field(default=3)
y: int = field(default_factory=lambda: 4)
# error: [dataclass-field-order]
z: str
import sys
@dataclass
class F:
x: int = 1
if sys.version_info > (3, 7):
# error: [dataclass-field-order]
y: str
Fields with init=False do not participate in the ordering check since they don't appear in
__init__:
@dataclass
class GoodWithInitFalse:
x: int = 1
y: str = field(init=False)
z: float = 2.0
@dataclass
class BadWithInitFalse:
x: int = 1
y: str = field(init=False)
# error: [dataclass-field-order] "Required field `z` cannot be defined after fields with default values"
z: float
Class-level init=False suppresses the ordering check because no constructor is generated:
@dataclass(init=False)
class GoodWithClassInitFalse:
x: int = 1
y: str
def __init__(self, y: str) -> None:
self.y = y
GoodWithClassInitFalse("value")
# Re-enabling `init` makes the inherited default-before-required ordering invalid at runtime.
# TODO: error: [dataclass-field-order]
@dataclass
class BadWithReenabledInit(GoodWithClassInitFalse):
pass
Keyword-only fields (using kw_only=True) also don't participate in the positional ordering check:
[environment]
python-version = "3.10"
@dataclass
class GoodWithKwOnly:
x: int = 1
y: str = field(kw_only=True)
z: float = field(kw_only=True, default=2.0)
@dataclass
class AlsoGoodWithKwOnly:
x: int = field(kw_only=True, default=1)
y: str
@dataclass
class BadWithKwOnly:
x: int = 1
y: str = field(kw_only=True)
# error: [dataclass-field-order] "Required field `z` cannot be defined after fields with default values"
z: float
Fields after a KW_ONLY sentinel are also keyword-only and don't participate in ordering checks:
from dataclasses import KW_ONLY
@dataclass
class GoodWithKwOnlySentinel:
x: int = 1
_: KW_ONLY
y: str
z: float = 2.0
@dataclass
class BadWithKwOnlySentinel:
x: int = 1
# error: [dataclass-field-order] "Required field `y` cannot be defined after fields with default values"
y: str
_: KW_ONLY
z: float
Pure class attributes (ClassVar) are not included in the signature of __init__:
from typing import ClassVar
@dataclass
class D:
x: int
y: ClassVar[str] = "default"
z: bool
reveal_type(D.__init__) # revealed: (self: D, x: int, z: bool) -> None
d = D(1, True)
reveal_type(d.x) # revealed: int
reveal_type(d.y) # revealed: str
reveal_type(d.z) # revealed: bool
Function declarations do not affect the signature of __init__:
@dataclass
class D:
x: int
def y(self) -> str:
return ""
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
And neither do nested class declarations:
@dataclass
class D:
x: int
class Nested:
y: str
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
But if there is a variable annotation with a function or class literal type, the signature of
__init__ will include this field:
from ty_extensions._internal import TypeOf
class SomeClass: ...
def some_function() -> None: ...
@dataclass
class D:
function_literal: TypeOf[some_function]
class_literal: TypeOf[SomeClass]
class_subtype_of: type[SomeClass]
# revealed: (self: D, function_literal: def some_function() -> None, class_literal: <class 'SomeClass'>, class_subtype_of: type[SomeClass]) -> None
reveal_type(D.__init__)
More realistically, dataclasses can have Callable attributes:
from typing import Callable
@dataclass
class D:
c: Callable[[int], str]
reveal_type(D.__init__) # revealed: (self: D, c: (int, /) -> str) -> None
Implicit instance attributes do not affect the signature of __init__:
@dataclass
class D:
x: int
def f(self, y: str) -> None:
self.y: str = y
reveal_type(D(1).y) # revealed: str
reveal_type(D.__init__) # revealed: (self: D, x: int) -> None
Annotating expressions does not lead to an entry in __annotations__ at runtime, and so it wouldn't
be included in the signature of __init__. This is a case that we currently don't detect:
@dataclass
class D:
# (x) is an expression, not a "simple name"
(x): int = 1 # fmt: skip
# TODO: should ideally not include a `x` parameter
reveal_type(D.__init__) # revealed:(self: D, x: int = 1) -> None
@dataclass calls with argumentsThe @dataclass decorator can take several arguments to customize the existence of the generated
methods. The following test makes sure that we still treat the class as a dataclass if (the default)
arguments are passed in:
from dataclasses import dataclass
@dataclass(init=True, repr=True, eq=True)
class Person:
name: str
age: int | None = None
alice = Person("Alice", 30)
reveal_type(repr(alice)) # revealed: str
reveal_type(alice == alice) # revealed: bool
A class is still treated as a dataclass when arguments are unpacked into the decorator:
from dataclasses import dataclass
SLOTS = {"slots": True}
@dataclass(frozen=True, **SLOTS)
class C:
x: int
reveal_type(C.__init__) # revealed: (self: C, x: int) -> None
reveal_type(C.__slots__) # revealed: tuple[Literal["x"]]
C(1)
If init is set to False, no __init__ method is generated:
from dataclasses import dataclass
@dataclass(init=False)
class C:
x: int
C() # Okay
# error: [too-many-positional-arguments]
C(1)
repr(C())
C() == C()
reprA custom __repr__ method is generated by default. It can be disabled by passing repr=False, but
in that case __repr__ is still available via object.__repr__:
from dataclasses import dataclass
@dataclass(repr=False)
class WithoutRepr:
x: int
reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str
eqThe same is true for __eq__. Setting eq=False disables the generated __eq__ method, but
__eq__ is still available via object.__eq__:
from dataclasses import dataclass
@dataclass(eq=False)
class WithoutEq:
x: int
reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
order[environment]
python-version = "3.12"
order is set to False by default. If order=True, __lt__, __le__, __gt__, and __ge__
methods will be generated:
from dataclasses import dataclass
@dataclass
class WithoutOrder:
x: int
WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator]
@dataclass(order=True)
class WithOrder:
x: int
WithOrder(1) < WithOrder(2)
WithOrder(1) <= WithOrder(2)
WithOrder(1) > WithOrder(2)
WithOrder(1) >= WithOrder(2)
order=True requires eq=True:
from dataclasses import dataclass
@dataclass(order=True, eq=False) # error: [invalid-dataclass] "`order=True` requires `eq=True`"
class InvalidOrder: ...
@dataclass
@dataclass(order=True, eq=False) # error: [invalid-dataclass] "`order=True` requires `eq=True`"
class InvalidStackedOrder: ...
@dataclass(order=True, eq=False, unexpected=True) # error: [no-matching-overload]
class InvalidArguments: ...
Comparisons are only allowed for WithOrder instances:
WithOrder(1) < 2 # error: [unsupported-operator]
WithOrder(1) <= 2 # error: [unsupported-operator]
WithOrder(1) > 2 # error: [unsupported-operator]
WithOrder(1) >= 2 # error: [unsupported-operator]
This also works for generic dataclasses:
from dataclasses import dataclass
@dataclass(order=True)
class GenericWithOrder[T]:
x: T
GenericWithOrder[int](1) < GenericWithOrder[int](1)
GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator]
Subclassing a dataclass with order=True is problematic because comparing instances of different
classes in the inheritance hierarchy will raise a TypeError at runtime. The design of the stdlib
feature therefore violates the Liskov Substitution Principle:
from dataclasses import dataclass
@dataclass(order=True)
class Parent:
value: int
class Child(Parent): # error: [subclass-of-dataclass-with-order]
pass
# The comparison methods generated by @dataclass(order=True) compare instances
# as tuples of their fields. At runtime, this raises TypeError when comparing
# instances of different classes in the hierarchy:
# Child(42) < Parent(42) # TypeError!
This also applies when the child class is also a dataclass:
@dataclass
class DataclassChild(Parent): # error: [subclass-of-dataclass-with-order]
y: str
If the child class also has order=True, a diagnostic is still emitted because cross-class
comparisons with the parent class still raise TypeError at runtime:
@dataclass(order=True)
class OrderedChild(Parent): # error: [subclass-of-dataclass-with-order]
y: str
If the child class manually overrides all comparison methods, the diagnostic is suppressed:
class ManualChild(Parent): # No warning - all comparison methods overridden
def __lt__(self, other: Parent) -> bool:
return True
def __le__(self, other: Parent) -> bool:
return True
def __gt__(self, other: Parent) -> bool:
return True
def __ge__(self, other: Parent) -> bool:
return True
Descendants of an ordered dataclass child are warned as well:
class Grandchild(OrderedChild): # error: [subclass-of-dataclass-with-order]
pass
@dataclass(order=True)
class OrderedGrandchild(OrderedChild): # error: [subclass-of-dataclass-with-order]
z: float
This also applies if the intermediate subclass is not itself a dataclass:
class IndirectGrandchild(Child): # error: [subclass-of-dataclass-with-order]
pass
If the parent dataclass does not have order=True, no warning is emitted:
@dataclass
class UnorderedParent:
x: int
class UnorderedChild(UnorderedParent): # No warning
pass
If a class already defines one of the comparison methods, a TypeError is raised at runtime and a
diagnostic is emitted.
@dataclass(order=True)
class InvalidCustomOrderDunderOverrides:
x: int
# error: [invalid-dataclass-override] "Cannot overwrite attribute `__lt__` in dataclass `InvalidCustomOrderDunderOverrides` with `order=True`"
def __lt__(self, other: object) -> bool:
return False
# error: [invalid-dataclass-override] "Cannot overwrite attribute `__le__` in dataclass `InvalidCustomOrderDunderOverrides` with `order=True`"
def __le__(self, other: object) -> bool:
return False
# error: [invalid-dataclass-override] "Cannot overwrite attribute `__gt__` in dataclass `InvalidCustomOrderDunderOverrides` with `order=True`"
def __gt__(self, other: object) -> bool:
return False
# error: [invalid-dataclass-override] "Cannot overwrite attribute `__ge__` in dataclass `InvalidCustomOrderDunderOverrides` with `order=True`"
def __ge__(self, other: object) -> bool:
return False
__hash__ and unsafe_hashIf eq and frozen are both True, a __hash__ method is generated by default:
from dataclasses import dataclass
@dataclass(eq=True, frozen=True)
class WithHash:
x: int
reveal_type(WithHash.__hash__) # revealed: (self: WithHash) -> int
If eq is set to True and frozen is set to False, __hash__ will be set to None, to mark
is unhashable (because it is mutable):
from dataclasses import dataclass
@dataclass(eq=True, frozen=False)
class WithoutHash:
x: int
reveal_type(WithoutHash.__hash__) # revealed: None
If eq is set to False, __hash__ will inherit from the parent class (which could be object).
Note that we see a revealed type of def … here, because __hash__ refers to an actual function,
not a synthetic method like in the first example.
from dataclasses import dataclass
from typing import Any
@dataclass(eq=False, frozen=False)
class InheritHash:
x: int
reveal_type(InheritHash.__hash__) # revealed: def __hash__(self) -> int
class Base:
# Type the `self` parameter as `Any` to distinguish it from `object.__hash__`
def __hash__(self: Any) -> int:
return 42
@dataclass(eq=False, frozen=False)
class InheritHash(Base):
x: int
reveal_type(InheritHash.__hash__) # revealed: def __hash__(self: Any) -> int
If unsafe_hash is set to True, a __hash__ method will be generated even if the dataclass is
mutable:
from dataclasses import dataclass
@dataclass(eq=True, frozen=False, unsafe_hash=True)
class WithUnsafeHash:
x: int
reveal_type(WithUnsafeHash.__hash__) # revealed: (self: WithUnsafeHash) -> int
frozenWhen frozen=True, a dataclass does not allow its fields to be assigned or deleted.
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int
frozen_instance = MyFrozenClass(1)
frozen_instance.x = 2 # error: [invalid-assignment]
reveal_type(frozen_instance.__delattr__) # revealed: (name) -> Never
# error: [invalid-assignment] "Cannot delete attribute `x` on type `MyFrozenClass` whose `__delattr__` method returns `Never`/`NoReturn`"
del frozen_instance.x
If __setattr__() or __delattr__() is defined in the class, a diagnostic is emitted.
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int
# error: [invalid-dataclass-override] "Cannot overwrite attribute `__setattr__` in frozen dataclass `MyFrozenClass`"
def __setattr__(self, name: str, value: object) -> None: ...
# error: [invalid-dataclass-override] "Cannot overwrite attribute `__delattr__` in frozen dataclass `MyFrozenClass`"
def __delattr__(self, name: str) -> None: ...
This also works for generic dataclasses:
[environment]
python-version = "3.12"
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenGeneric[T]:
x: T
frozen_instance = MyFrozenGeneric[int](1)
frozen_instance.x = 2 # error: [invalid-assignment]
Attempting to mutate an unresolved attribute on a frozen dataclass:
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass: ...
frozen = MyFrozenClass()
frozen.x = 2 # error: [invalid-assignment] "Cannot assign to unresolved attribute `x` on type `MyFrozenClass`"
A diagnostic is also emitted if a frozen dataclass is inherited, and an attempt is made to mutate an attribute in the child class:
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1
class MyFrozenChildClass(MyFrozenClass): ...
class MyFrozenGrandchildClass(MyFrozenChildClass): ...
frozen = MyFrozenChildClass()
frozen.x = 2 # error: [invalid-assignment]
grandchild = MyFrozenGrandchildClass()
grandchild.x = 2 # error: [invalid-assignment]
Non-field attributes on a non-dataclass subclass of a frozen dataclass are still assignable:
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1
class MyFrozenChildClass(MyFrozenClass):
y: int
class MyFrozenGrandchildClass(MyFrozenChildClass):
z: int
frozen = MyFrozenChildClass()
frozen.y = 2
frozen.z = 2
grandchild = MyFrozenGrandchildClass()
grandchild.y = 2
grandchild.z = 2
grandchild.unknown = 2
The synthesized __setattr__ is exposed as a member on non-dataclass subclasses of frozen
dataclasses:
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1
class MyFrozenChildClass(MyFrozenClass): ...
child = MyFrozenChildClass()
reveal_type(child.__setattr__) # revealed: Overload[(name: Literal["x"], value) -> Never, (name: str, value) -> None]
An explicit __setattr__ on an intermediate subclass overrides the inherited frozen dataclass
setter:
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1
class MyFrozenIntermediateClass(MyFrozenClass):
def __setattr__(self, name: str, value: object) -> None: ... # error: [invalid-method-override]
class MyFrozenChildClass(MyFrozenIntermediateClass):
y: int
class MyFrozenGrandchildClass(MyFrozenChildClass):
z: int
child = MyFrozenChildClass()
child.x = 2
child.y = 2
child.unknown = 2
grandchild = MyFrozenGrandchildClass()
grandchild.x = 2
grandchild.y = 2
grandchild.z = 2
grandchild.unknown = 2
When another base class rejects assignment, a frozen dataclass must not hide its __setattr__
method:
from dataclasses import dataclass
from typing import NoReturn
@dataclass(frozen=True)
class Frozen:
x: int = 1
class RejectsAssignment:
y: int = 1
def __setattr__(self, name: str, value: object) -> NoReturn:
raise AttributeError(name)
class ChildWithRejectingAssignmentBase(Frozen, RejectsAssignment): ...
# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithRejectingAssignmentBase` whose `__setattr__` method returns `Never`/`NoReturn`"
ChildWithRejectingAssignmentBase().y = 2
A later base class can customize assignment to an ordinary attribute. The value must satisfy both
the later __setattr__ and the attribute declaration:
class AllowsAssignment:
y: object = 1
def __setattr__(self, name: str, value: int) -> None: ...
class ChildWithAllowingAssignmentBase(Frozen, AllowsAssignment): ...
allowed = ChildWithAllowingAssignmentBase()
allowed.y = 2
# error: [invalid-assignment] "Cannot assign object of type"
allowed.y = "invalid"
A later __setattr__ can forward to object.__setattr__, which still invokes data descriptors:
class ForwardsAssignment:
def __setattr__(self, name: str, value: object) -> None:
super().__setattr__(name, value)
A read-only property therefore remains read-only:
class ReadOnlyPropertyBase(ForwardsAssignment):
@property
def y(self) -> int:
return 1
class ChildWithReadOnlyProperty(Frozen, ReadOnlyPropertyBase): ...
# error: [invalid-assignment] "Cannot assign to read-only property `y` on object of type `ChildWithReadOnlyProperty`"
ChildWithReadOnlyProperty().y = 2
The property's setter still determines which values it accepts:
class TypedPropertyBase(ForwardsAssignment):
@property
def y(self) -> int:
return 1
@y.setter
def y(self, value: int) -> None: ...
class ChildWithTypedProperty(Frozen, TypedPropertyBase): ...
# error: [invalid-assignment] "Expected `int`, found `Literal["invalid"]`"
ChildWithTypedProperty().y = "invalid"
A property setter that never returns prevents assignment:
class TerminalPropertyBase(ForwardsAssignment):
@property
def y(self) -> int:
return 1
@y.setter
def y(self, value: int) -> NoReturn:
raise AttributeError
class ChildWithTerminalProperty(Frozen, TerminalPropertyBase): ...
# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithTerminalProperty` whose `__set__` method returns `Never`/`NoReturn`"
ChildWithTerminalProperty().y = 2
The same rule applies to a custom descriptor whose setter never returns:
class TerminalDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: object, value: int) -> NoReturn:
raise AttributeError
class TerminalDescriptorBase(ForwardsAssignment):
y: TerminalDescriptor = TerminalDescriptor()
class ChildWithTerminalDescriptor(Frozen, TerminalDescriptorBase): ...
# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithTerminalDescriptor` whose `__set__` method returns `Never`/`NoReturn`"
ChildWithTerminalDescriptor().y = 2
A later __setattr__ does not make the declared type of an ordinary attribute disappear:
class AllowsUntypedAssignment:
y: int = 1
def __setattr__(self, name: str, value: object) -> None: ...
class ChildWithUntypedAssignmentBase(Frozen, AllowsUntypedAssignment): ...
# error: [invalid-assignment]
ChildWithUntypedAssignmentBase().y = "invalid"
A specialized Generic[T] frozen base must also preserve the next __setattr__ in the MRO:
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass(frozen=True)
class GenericFrozen(Generic[T]):
value: T
class RejectingGenericAssignmentChild(GenericFrozen[int], RejectsAssignment): ...
# error: [invalid-assignment] "Cannot assign to attribute `y` on type `RejectingGenericAssignmentChild` whose `__setattr__` method returns `Never`/`NoReturn`"
RejectingGenericAssignmentChild(1).y = 2
The same behavior applies to Python 3.12 type-parameter syntax:
@dataclass(frozen=True)
class TypeParameterFrozen[T]:
value: T
class RejectingTypeParameterAssignmentChild(TypeParameterFrozen[int], RejectsAssignment): ...
# error: [invalid-assignment] "Cannot assign to attribute `y` on type `RejectingTypeParameterAssignmentChild` whose `__setattr__` method returns `Never`/`NoReturn`"
RejectingTypeParameterAssignmentChild(1).y = 2
When a subclass inherits from two frozen dataclasses, fields from both bases remain frozen:
@dataclass(frozen=True)
class FirstFrozen:
first: int = 1
@dataclass(frozen=True)
class SecondFrozen:
second: int = 1
class ChildWithTwoFrozenBases(FirstFrozen, SecondFrozen): ...
multiple = ChildWithTwoFrozenBases()
# revealed: Overload[(name: Literal["first"], value) -> Never, (name: Literal["second"], value) -> Never, (name: str, value) -> None]
reveal_type(multiple.__setattr__)
# revealed: Overload[(name: Literal["first"]) -> Never, (name: Literal["second"]) -> Never, (name: str) -> None]
reveal_type(multiple.__delattr__)
multiple.second = 2 # error: [invalid-assignment]
del multiple.second # error: [invalid-assignment]
An InitVar is a constructor argument, not a frozen field. A subclass can assign and delete an
attribute with the same name:
from dataclasses import InitVar
@dataclass(frozen=True)
class FrozenWithInitVar:
temporary: InitVar[int] = 0
class ChildWithInitVar(FrozenWithInitVar):
temporary: int = 1
init_var_child = ChildWithInitVar()
init_var_child.temporary = 4
del init_var_child.temporary
The same rule applies when the InitVar belongs to a second frozen base:
class ChildWithSecondBaseInitVar(Frozen, FrozenWithInitVar):
temporary: int = 1
second_init_var_child = ChildWithSecondBaseInitVar()
second_init_var_child.temporary = 4
del second_init_var_child.temporary
Non-field attributes on subclasses of slotted frozen dataclasses are still rejected. This correctly
models the runtime behavior, but is somewhat surprising and may be a CPython bug, as subclasses of
slotted classes usually allow arbitrary attributes to be set on them unless the subclass also
explicitly declares __slots__. We should change our behavior here to follow CPython, if they "fix"
it. The same limitation applies when deleting an attribute.
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class MySlottedFrozenClass:
x: int = 1
class MySlottedFrozenChildClass(MySlottedFrozenClass):
y: int
class MySlottedFrozenGrandchildClass(MySlottedFrozenChildClass):
z: int
frozen = MySlottedFrozenChildClass()
frozen.x = 2 # error: [invalid-assignment]
frozen.y = 2 # error: [invalid-assignment]
frozen.z = 2 # error: [invalid-assignment]
del frozen.x # error: [invalid-assignment]
del frozen.y # error: [invalid-assignment]
grandchild = MySlottedFrozenGrandchildClass()
grandchild.x = 2 # error: [invalid-assignment]
grandchild.y = 2 # error: [invalid-assignment]
grandchild.z = 2 # error: [invalid-assignment]
grandchild.unknown = 2 # error: [invalid-assignment]
A frozen dataclass also prevents an ordinary subclass from deleting an inherited field:
from dataclasses import dataclass
@dataclass(frozen=True)
class MyFrozenClass:
x: int = 1
class MyFrozenChildClass(MyFrozenClass): ...
frozen = MyFrozenChildClass()
# revealed: Overload[(name: Literal["x"]) -> Never, (name: str) -> None]
reveal_type(frozen.__delattr__)
del frozen.x # error: [invalid-assignment]
A frozen dataclass does not make a subclass's read-only property safe to delete:
from dataclasses import dataclass
@dataclass(frozen=True)
class Frozen:
x: int = 1
class ReadOnlyChild(Frozen):
@property
def y(self) -> int:
return 1
# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ReadOnlyChild`"
del ReadOnlyChild().y
Deleting a property is also invalid when its deleter never returns:
from typing import NoReturn
class RejectingPropertyChild(Frozen):
@property
def y(self) -> int:
return 1
@y.deleter
def y(self) -> NoReturn:
raise AttributeError("y")
# error: [invalid-assignment] "Cannot delete attribute `y` on type `RejectingPropertyChild` whose `__delete__` method returns `Never`/`NoReturn`"
del RejectingPropertyChild().y
When another base class rejects deletion, the frozen dataclass must not hide its __delattr__
method:
class RejectsDeletion:
y: int = 1
def __delattr__(self, name: str) -> NoReturn:
raise AttributeError(name)
class ChildWithRejectingBase(Frozen, RejectsDeletion): ...
# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithRejectingBase` whose `__delattr__` method returns `Never`/`NoReturn`"
del ChildWithRejectingBase().y
A second base class can customize deletion of an ordinary attribute:
class AllowsDeletion:
y: int = 1
def __delattr__(self, name: str) -> None: ...
class ChildWithAllowingBase(Frozen, AllowsDeletion): ...
del ChildWithAllowingBase().y
A later __delattr__ can forward to object.__delattr__, which still invokes data descriptors:
class ForwardsDeletion:
def __delattr__(self, name: str) -> None:
super().__delattr__(name)
A read-only property therefore remains read-only:
class ReadOnlyDeletionBase(ForwardsDeletion):
@property
def y(self) -> int:
return 1
class ChildWithReadOnlyDeletion(Frozen, ReadOnlyDeletionBase): ...
# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ChildWithReadOnlyDeletion`"
del ChildWithReadOnlyDeletion().y
A property deleter that never returns also prevents deletion:
class TerminalDeletionBase(ForwardsDeletion):
@property
def y(self) -> int:
return 1
@y.deleter
def y(self) -> NoReturn:
raise AttributeError
class ChildWithTerminalDeletion(Frozen, TerminalDeletionBase): ...
# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithTerminalDeletion` whose `__delete__` method returns `Never`/`NoReturn`"
del ChildWithTerminalDeletion().y
The same rule applies to a custom descriptor whose deleter never returns:
class TerminalDeleteDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __delete__(self, instance: object) -> NoReturn:
raise AttributeError
class TerminalDescriptorDeletionBase(ForwardsDeletion):
y: TerminalDeleteDescriptor = TerminalDeleteDescriptor()
class ChildWithTerminalDescriptorDeletion(Frozen, TerminalDescriptorDeletionBase): ...
# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithTerminalDescriptorDeletion` whose `__delete__` method returns `Never`/`NoReturn`"
del ChildWithTerminalDescriptorDeletion().y
An ordinary attribute defined on a subclass can also be deleted:
class ChildWithOwnAttribute(Frozen):
y: int = 1
deletable = ChildWithOwnAttribute()
deletable.y = 2
del deletable.y
A subclass can replace the inherited __delattr__, but a method that returns None is an invalid
override of the frozen base's method, which returns Never. The overriding method still controls
deletion on both that subclass and its subclasses:
class ChildWithDeletionOverride(Frozen):
# error: [invalid-method-override]
def __delattr__(self, name: str) -> None: ...
class GrandchildWithDeletionOverride(ChildWithDeletionOverride): ...
del ChildWithDeletionOverride().x
del GrandchildWithDeletionOverride().x
A read-only property remains protected when the frozen base is a specialized Generic[T] dataclass:
class ReadOnlyGenericChild(GenericFrozen[int]):
@property
def y(self) -> int:
return 1
# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ReadOnlyGenericChild`"
del ReadOnlyGenericChild(1).y
The Python 3.12 type-parameter syntax must also preserve a __delattr__ method defined by another
base class:
class RejectingTypeParameterChild(TypeParameterFrozen[int], RejectsDeletion): ...
# error: [invalid-assignment] "Cannot delete attribute `y` on type `RejectingTypeParameterChild` whose `__delattr__` method returns `Never`/`NoReturn`"
del RejectingTypeParameterChild(1).y
If a non-frozen dataclass inherits from a frozen dataclass, an exception is raised at runtime. We catch this error:
foo.py:
from dataclasses import dataclass
@dataclass(frozen=True)
class FrozenBase:
x: int
@dataclass
# snapshot: invalid-frozen-dataclass-subclass
class Child(FrozenBase):
y: int
error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass
--> src/foo.py:9:7
|
7 | @dataclass
| ---------- `Child` dataclass parameters
8 | # snapshot: invalid-frozen-dataclass-subclass
9 | class Child(FrozenBase):
| ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is
info: This causes the class creation to fail
info: Base class definition
--> src/foo.py:4:7
|
3 | @dataclass(frozen=True)
| ----------------------- `FrozenBase` dataclass parameters
4 | class FrozenBase:
| ^^^^^^^^^^ `FrozenBase` definition
Frozen dataclasses inheriting from non-frozen dataclasses are also illegal:
bar.py:
from dataclasses import dataclass
@dataclass
class Base:
x: int
@dataclass(frozen=True)
# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
class FrozenChild(Base):
y: int
Example of diagnostics when there are multiple files involved:
module.py:
import dataclasses
@dataclasses.dataclass(frozen=False)
class NotFrozenBase:
x: int
main.py:
from functools import total_ordering
from typing import final
from dataclasses import dataclass
from module import NotFrozenBase
@final
@dataclass(frozen=True)
@total_ordering # error: [invalid-total-ordering]
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
y: str
match_argsIf match_args is set to True (the default), the __match_args__ attribute is a tuple created
from the list of non keyword-only parameters to the synthesized __init__ method (even if
__init__ is not actually generated).
from dataclasses import dataclass, field
@dataclass
class WithMatchArgs:
normal_a: str
normal_b: int
kw_only: int = field(kw_only=True)
reveal_type(WithMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]]
@dataclass(kw_only=True)
class KwOnlyDefaultMatchArgs:
normal_a: str = field(kw_only=False)
normal_b: int = field(kw_only=False)
kw_only: int
reveal_type(KwOnlyDefaultMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]]
@dataclass(match_args=True)
class ExplicitMatchArgs:
normal: str
reveal_type(ExplicitMatchArgs.__match_args__) # revealed: tuple[Literal["normal"]]
@dataclass
class Empty: ...
reveal_type(Empty.__match_args__) # revealed: tuple[()]
When match_args is explicitly set to False, the __match_args__ attribute is not available:
@dataclass(match_args=False)
class NoMatchArgs:
x: int
y: str
NoMatchArgs.__match_args__ # error: [unresolved-attribute]
kw_onlyAn error is emitted if a dataclass is defined with kw_only=True and positional arguments are
passed to the constructor.
[environment]
python-version = "3.10"
from dataclasses import dataclass
@dataclass(kw_only=True)
class A:
x: int
y: int
# error: [missing-argument] "No arguments provided for required parameters `x`, `y`"
# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 2"
a = A(1, 2)
a = A(x=1, y=2)
The class-level parameter can be overridden per-field.
from dataclasses import dataclass, field
@dataclass(kw_only=True)
class A:
a: str = field(kw_only=False)
b: int = 0
reveal_type(A.__init__) # revealed:(self: A, a: str, *, b: int = 0) -> None
A("hi")
If some fields are kw_only, they should appear after all positional fields in the __init__
signature.
@dataclass
class A:
b: int = field(kw_only=True, default=3)
a: str
A("hi")
The field-level kw_only value takes precedence over the KW_ONLY pseudo-type.
from dataclasses import field, dataclass, KW_ONLY
@dataclass
class C:
_: KW_ONLY
x: int = field(kw_only=False)
C(x=1)
C(1)
No fields of a kw_only=True dataclass participate in field ordering checks.
from dataclasses import dataclass, field
@dataclass(kw_only=True)
class KwOnlyClassGood:
x: int = 1
y: str
@dataclass(kw_only=True)
class KwOnlyClassAlsoGood:
x: int
y: str = "default"
z: float
The class-level kw_only parameter can be overridden per-field.
@dataclass(kw_only=True)
class KwOnlyClassWithPositionalField:
x: int = 1
y: str = field(kw_only=False, default="hello")
# error: [dataclass-field-order] "Required field `z` cannot be defined after fields with default values"
z: float = field(kw_only=False)
kw_only - Python < 3.10For Python < 3.10, kw_only is not supported.
[environment]
python-version = "3.9"
from dataclasses import dataclass
@dataclass(kw_only=True) # error: [no-matching-overload]
class A:
x: int
y: int
kw_only - Python 3.13[environment]
python-version = "3.13"
from dataclasses import dataclass, field
@dataclass
class Employee:
e_id: int = field(kw_only=True, default=0)
name: str
Employee("Alice")
Employee(name="Alice")
Employee(name="Alice", e_id=1)
Employee(e_id=1, name="Alice")
Employee("Alice", e_id=1)
Employee("Alice", 1) # error: [too-many-positional-arguments]
kw_only - Python 3.14[environment]
python-version = "3.14"
from dataclasses import dataclass, field
@dataclass
class Employee:
# Python 3.14 introduces a new `doc` parameter for `dataclasses.field`
e_id: int = field(kw_only=True, default=0, doc="Global employee ID")
name: str
Employee("Alice")
Employee(name="Alice")
Employee(name="Alice", e_id=1)
Employee(e_id=1, name="Alice")
Employee("Alice", e_id=1)
Employee("Alice", 1) # error: [too-many-positional-arguments]
kw_onlyWhen a child dataclass uses @dataclass(kw_only=True), the kw_only setting should only apply to
fields defined in the child class, not to inherited fields from parent classes.
This is a regression test for https://github.com/astral-sh/ty/issues/1769.
[environment]
python-version = "3.10"
from dataclasses import dataclass
@dataclass
class Inner:
inner: int
@dataclass(kw_only=True)
class Outer(Inner):
outer: int
# Inherited field `inner` is positional, new field `outer` is keyword-only
reveal_type(Outer.__init__) # revealed: (self: Outer, inner: int, *, outer: int) -> None
Outer(0, outer=5) # OK
Outer(inner=0, outer=5) # Also OK
# error: [missing-argument]
# error: [too-many-positional-arguments]
Outer(0, 5)
This also works when the parent class uses the KW_ONLY sentinel:
from dataclasses import dataclass, KW_ONLY
@dataclass
class Parent:
a: int
_: KW_ONLY
b: str
@dataclass(kw_only=True)
class Child(Parent):
c: bytes
# `a` is positional (from parent), `b` is keyword-only (from parent's KW_ONLY),
# `c` is keyword-only (from child's kw_only=True)
reveal_type(Child.__init__) # revealed: (self: Child, a: int, *, b: str, c: bytes) -> None
Child(1, b="hello", c=b"world") # OK
# error: [missing-argument] "No arguments provided for required parameters `b`, `c`"
# error: [too-many-positional-arguments]
Child(1, "hello", b"world")
And when the child class uses the KW_ONLY sentinel while inheriting from a parent:
from dataclasses import dataclass, KW_ONLY
@dataclass
class Base:
x: int
@dataclass
class Derived(Base):
y: str
_: KW_ONLY
z: bytes
# `x` and `y` are positional, `z` is keyword-only (from Derived's KW_ONLY)
reveal_type(Derived.__init__) # revealed: (self: Derived, x: int, y: str, *, z: bytes) -> None
Derived(1, "hello", z=b"world") # OK
# error: [missing-argument]
# error: [too-many-positional-arguments]
Derived(1, "hello", b"world")
The reverse case also works: when a parent has kw_only=True but the child doesn't, the parent's
fields stay keyword-only while the child's fields are positional:
from dataclasses import dataclass
@dataclass(kw_only=True)
class KwOnlyParent:
parent_field: int
@dataclass
class PositionalChild(KwOnlyParent):
child_field: str
# `child_field` is positional (child's default), `parent_field` stays keyword-only
reveal_type(PositionalChild.__init__) # revealed: (self: PositionalChild, child_field: str, *, parent_field: int) -> None
PositionalChild("hello", parent_field=1) # OK
# error: [missing-argument]
# error: [too-many-positional-arguments]
PositionalChild("hello", 1)
slotsIf a dataclass is defined with slots=True, the __slots__ attribute is generated as a tuple. It
is not present otherwise.
from dataclasses import dataclass
from typing import Tuple
@dataclass
class A:
x: int
y: int
# revealed: Unknown
# error: [unresolved-attribute]
reveal_type(A.__slots__)
@dataclass(slots=True)
class B:
x: int
y: int
reveal_type(B.__slots__) # revealed: tuple[Literal["x"], Literal["y"]]
weakref_slotWhen a dataclass is defined with weakref_slot=True on Python >=3.11, the __weakref__ attribute
is generated. For now, we do not attempt to infer a more precise type for it.
[environment]
python-version = "3.11"
from dataclasses import dataclass
@dataclass(slots=True, weakref_slot=True)
class C:
x: int
reveal_type(C.__weakref__) # revealed: Any | None
weakref_slot=True requires slots=True:
from dataclasses import dataclass
@dataclass(weakref_slot=True) # error: [invalid-dataclass] "`weakref_slot=True` requires `slots=True`"
class InvalidWeakrefSlot: ...
When slots is a non-literal bool, the combination might be valid at runtime, so we don't report
an error:
from dataclasses import dataclass
def dynamic_slots(slots: bool):
@dataclass(weakref_slot=True, slots=slots)
class PossiblyValidWeakrefSlot: ...
An invalid combination is reported when the decorator factory is called, even if the decorator is applied later:
from dataclasses import dataclass
invalid_weakref_slot = dataclass(weakref_slot=True) # error: [invalid-dataclass] "`weakref_slot=True` requires `slots=True`"
class AppliedWeakrefSlotLater: ...
invalid_weakref_slot(AppliedWeakrefSlotLater)
The __weakref__ attribute is correctly not modeled as existing on instances of slotted dataclasses
where the class definition was not marked with weakref=True:
from dataclasses import dataclass
@dataclass(slots=True)
class C: ...
# error: [unresolved-attribute]
reveal_type(C().__weakref__) # revealed: Unknown
Certain parameters to @dataclass were added on newer Python versions; we do not infer them as
having any effect on older Python versions:
[environment]
python-version = "3.9"
from dataclasses import dataclass
# fmt: off
@dataclass( # error: [no-matching-overload]
slots=True,
weakref_slot=True,
match_args=True
)
class Foo: ...
# fmt: on
# error: [unresolved-attribute]
reveal_type(Foo.__slots__) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(Foo.__match_args__) # revealed: Unknown
# TODO: this actually *does* exist at runtime
# (all classes and non-slotted instances have it available by default).
# We could try to model that more fully...?
# It's not added by the dataclasses machinery, though.
#
# error: [unresolved-attribute]
reveal_type(Foo.__weakref__) # revealed: Unknown
Final fieldsDataclass fields can be annotated with Final, which means that the field cannot be reassigned
after the instance is created. Fields that are additionally annotated with ClassVar are not part
of the __init__ signature.
from dataclasses import dataclass
from typing import Final, ClassVar
@dataclass
class C:
# a `Final` annotation without a right-hand side is not allowed in normal classes,
# but valid for dataclasses. The field will be initialized in the synthesized
# `__init__` method
instance_variable_no_default: Final[int]
instance_variable: Final[int] = 1
class_variable1: ClassVar[Final[int]] = 1
class_variable2: ClassVar[Final[int]] = 1
reveal_type(C.__init__) # revealed:(self: C, instance_variable_no_default: int, instance_variable: int = 1) -> None
c = C(1)
# error: [invalid-assignment] "Cannot assign to final attribute `instance_variable` on type `C`"
c.instance_variable = 2
from dataclasses import dataclass
@dataclass
class Base:
x: int
class Derived(Base): ...
d = Derived(1) # OK
reveal_type(d.x) # revealed: int
from dataclasses import dataclass
class Base:
x: int = 1
@dataclass
class Derived(Base):
y: str
d = Derived("a")
# error: [too-many-positional-arguments]
# error: [invalid-argument-type]
Derived(1, "a")
from dataclasses import dataclass
@dataclass
class Base:
x: int
y: str
@dataclass
class Derived(Base):
z: bool
d = Derived(1, "a", True) # OK
reveal_type(d.x) # revealed: int
reveal_type(d.y) # revealed: str
reveal_type(d.z) # revealed: bool
# error: [missing-argument]
Derived(1, "a")
# error: [missing-argument]
Derived(True)
The following example comes from the
Python documentation. The x
attribute appears just once in the __init__ signature, and the default value is taken from the
derived class
from dataclasses import dataclass
from typing import Any
@dataclass
class Base:
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 15
reveal_type(C.__init__) # revealed:(self: C, x: int = 15, y: int = 0, z: int = 10) -> None
Fields that are defined in always-reachable branches are always present in the synthesized
__init__ method. Fields that are defined in never-reachable branches are not present:
from dataclasses import dataclass
@dataclass
class C:
normal: int
if 1 + 2 == 3:
always_present: str
if 1 + 2 == 4:
never_present: bool
reveal_type(C.__init__) # revealed: (self: C, normal: int, always_present: str) -> None
If a field is conditionally defined, we currently assume that it is always present. A more complex
alternative here would be to synthesized a union of all possible __init__ signatures:
from dataclasses import dataclass
def flag() -> bool:
return True
@dataclass
class C:
normal: int
if flag():
conditionally_present: str
reveal_type(C.__init__) # revealed: (self: C, normal: int, conditionally_present: str) -> None
[environment]
python-version = "3.12"
from dataclasses import dataclass
@dataclass
class DataWithDescription[T]:
data: T
description: str
reveal_type(DataWithDescription[int]) # revealed: <class 'DataWithDescription[int]'>
d_int = DataWithDescription[int](1, "description") # OK
reveal_type(d_int.data) # revealed: int
reveal_type(d_int.description) # revealed: str
# error: [invalid-argument-type]
DataWithDescription[int](None, "description")
This is a regression test for https://github.com/astral-sh/ty/issues/853.
from dataclasses import dataclass
@dataclass
class Wrap[T]:
data: T
reveal_type(Wrap[int].__init__) # revealed: (self: Wrap[int], data: int) -> None
@dataclass
class WrappedInt(Wrap[int]):
other_field: str
reveal_type(WrappedInt.__init__) # revealed: (self: WrappedInt, data: int, other_field: str) -> None
# Make sure that another generic type parameter does not affect the `data` field
@dataclass
class WrappedIntAndExtraData[T](Wrap[int]):
extra_data: T
# revealed: (self: WrappedIntAndExtraData[bytes], data: int, extra_data: bytes) -> None
reveal_type(WrappedIntAndExtraData[bytes].__init__)
This is a regression test for https://github.com/astral-sh/ty/issues/1427.
When a non-dataclass inherits from a generic dataclass, the generic type parameters should still be
properly inferred when calling the inherited __init__ method.
from dataclasses import dataclass
@dataclass
class ParentDataclass[T]:
value: T
# Non-dataclass inheriting from generic dataclass
class ChildOfParentDataclass[T](ParentDataclass[T]): ...
def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]:
return ChildOfParentDataclass(x)
# TODO: ParentDataclass.__init__ should show generic types, not Unknown
# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None
reveal_type(ParentDataclass.__init__)
# revealed: [T](self: ParentDataclass[T], value: T) -> None
reveal_type(ChildOfParentDataclass.__init__)
result_int = uses_dataclass(42)
reveal_type(result_int) # revealed: ChildOfParentDataclass[int]
result_str = uses_dataclass("hello")
reveal_type(result_str) # revealed: ChildOfParentDataclass[str]
__get__ and __set__For the following descriptor, the return type of __get__ and the type of the value parameter in
__set__ are the same. The generated __init__ method takes an argument of this type (instead of
the type of the descriptor), and the default value is also of this type:
from typing import overload
from dataclasses import dataclass
class UppercaseString:
_value: str = ""
def __get__(self, instance: object, owner: None | type) -> str:
return self._value
def __set__(self, instance: object, value: str) -> None:
self._value = value.upper()
@dataclass
class C:
upper: UppercaseString = UppercaseString()
reveal_type(C.__init__) # revealed: (self: C, upper: str = ...) -> None
c = C("abc")
reveal_type(c.upper) # revealed: str
# This is also okay:
C()
# error: [invalid-argument-type]
C(1)
# error: [too-many-positional-arguments]
C("a", "b")
__get__ and __set__In general, the type of the __init__ parameter is determined by the value parameter type of the
__set__ method (str in the example below). However, the default value is generated by calling
the descriptor's __get__ method as if it had been called on the class itself, i.e. passing None
for the instance argument.
from typing import Literal, overload
from dataclasses import dataclass
class ConvertToLength:
_len: int = 0
@overload
def __get__(self, instance: None, owner: type) -> Literal[""]: ...
@overload
def __get__(self, instance: object, owner: type | None) -> int: ...
def __get__(self, instance: object | None, owner: type | None) -> str | int:
if instance is None:
return ""
return self._len
def __set__(self, instance, value: str) -> None:
self._len = len(value)
@dataclass
class C:
converter: ConvertToLength = ConvertToLength()
reveal_type(C.__init__) # revealed: (self: C, converter: str = "") -> None
c = C("abc")
reveal_type(c.converter) # revealed: int
# This is also okay:
C()
# error: [invalid-argument-type]
C(1)
# error: [too-many-positional-arguments]
C("a", "b")
__set__ methodIf the __set__ method is overloaded, we determine the type for the __init__ parameter as the
union of all possible value parameter types:
from typing import overload
from dataclasses import dataclass
class AcceptsStrAndInt:
def __get__(self, instance, owner) -> int:
return 0
@overload
def __set__(self, instance: object, value: str) -> None: ...
@overload
def __set__(self, instance: object, value: int) -> None: ...
def __set__(self, instance: object, value) -> None:
pass
@dataclass
class C:
field: AcceptsStrAndInt = AcceptsStrAndInt()
reveal_type(C.__init__) # revealed: (self: C, field: str | int = ...) -> None
__set__ typesWhen the descriptor type is an intersection, the generated __init__ parameter should use the
intersection of the acceptable value types from __set__.
from dataclasses import dataclass
from typing import Callable, cast
from ty_extensions import Intersection
class A: ...
class B: ...
def set_a(self: "DescA", instance: object, value: A) -> None: ...
def set_b(self: "DescB", instance: object, value: B) -> None: ...
class DescA:
# We use callable attributes instead of regular methods here because regular methods currently
# trigger a separate known issue where method attributes on intersections can collapse to `Never`:
# https://github.com/astral-sh/ty/issues/2428
__set__: Callable[["DescA", object, A], None] = set_a
class DescB:
__set__: Callable[["DescB", object, B], None] = set_b
@dataclass
class C:
field: Intersection[DescA, DescB] = cast(
Intersection[DescA, DescB],
DescA(),
)
reveal_type(C.__init__) # revealed: (self: C, field: A & B = ...) -> None
dataclasses.fieldTo do
dataclass.fieldsDataclasses have a special __dataclass_fields__ class variable member. The DataclassInstance
protocol checks for the presence of this attribute. It is used in the dataclasses.fields and
dataclasses.asdict functions, for example:
from dataclasses import dataclass, fields, asdict
@dataclass
class Foo:
x: int
foo = Foo(1)
reveal_type(foo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
reveal_type(type(foo).__dataclass_fields__) # revealed: dict[str, Field[Any]]
reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...]
reveal_type(asdict(foo)) # revealed: dict[str, Any]
The class objects themselves also have a __dataclass_fields__ attribute:
reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
They can be passed into fields as well, because it also accepts type[DataclassInstance]
arguments:
reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...]
But calling asdict on the class object is not allowed:
# error: [invalid-argument-type] "Argument to function `asdict` is incorrect: Expected `DataclassInstance`, found `<class 'Foo'>`"
asdict(Foo)
dataclasses.is_dataclassis_dataclass recognizes both dataclass instances and dataclass classes. A concrete dataclass
instance always satisfies the DataclassInstance protocol, so the negative branch is unreachable:
from dataclasses import dataclass, is_dataclass
@dataclass
class Event:
x: int
def check(event: Event) -> None:
if not is_dataclass(event):
reveal_type(event) # revealed: Never
dataclasses.KW_ONLYIf an attribute is annotated with dataclasses.KW_ONLY, it is not added to the synthesized
__init__ of the class. Instead, this special marker annotation causes Python at runtime to ensure
that all annotations following it have keyword-only parameters generated for them in the class's
synthesized __init__ method.
[environment]
python-version = "3.10"
from dataclasses import dataclass, field, KW_ONLY
@dataclass
class C:
x: int
_: KW_ONLY
y: str
reveal_type(C.__init__) # revealed: (self: C, x: int, *, y: str) -> None
# snapshot: missing-argument
# snapshot: too-many-positional-arguments
C(3, "")
C(3, y="")
error[missing-argument]: No argument provided for required parameter `y`
--> src/mdtest_snippet.py:13:1
|
13 | C(3, "")
| ^^^^^^^^
error[too-many-positional-arguments]: Too many positional arguments: expected 1, got 2
--> src/mdtest_snippet.py:13:6
|
13 | C(3, "")
| ^^
Declaration order still controls KW_ONLY when a later field name was already referenced by an
earlier annotation:
@dataclass
class ShadowedOrder:
x: int
_: KW_ONLY
int: int
reveal_type(ShadowedOrder.__init__) # revealed: (self: ShadowedOrder, x: int, *, int: int) -> None
Using KW_ONLY to annotate more than one field in a dataclass causes a TypeError to be raised at
runtime:
@dataclass
class Fails: # error: [duplicate-kw-only]
a: int
b: KW_ONLY
c: str
d: KW_ONLY
e: bytes
reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None
This also works if KW_ONLY is used in a conditional branch:
def flag() -> bool:
return True
@dataclass
class D: # error: [duplicate-kw-only]
x: int
_1: KW_ONLY
if flag():
y: str
_2: KW_ONLY
z: float
KW_ONLY should only affect fields declared after it within the same class, not fields in
subclasses:
from dataclasses import dataclass, KW_ONLY
@dataclass
class D:
x: int
_: KW_ONLY
y: str
@dataclass
class E(D):
z: bytes
# This should work: x=1 (positional), z=b"foo" (positional), y="foo" (keyword-only)
E(1, b"foo", y="foo")
reveal_type(E.__init__) # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
dataclasses.dataclassWe also understand dataclasses if they are decorated with the fully qualified name:
import dataclasses
@dataclasses.dataclass
class C:
x: str
reveal_type(C.__init__) # revealed: (self: C, x: str) -> None
__init__ methodIf a class already defines __init__, it is not replaced by the dataclass decorator.
from dataclasses import dataclass
@dataclass(init=True)
class C:
x: str
def __init__(self, x: int) -> None:
self.x = str(x)
C(1) # OK
# error: [invalid-argument-type]
C("a")
Similarly, if we set init=False, we still recognize the custom __init__ method:
@dataclass(init=False)
class D:
def __init__(self, x: int) -> None:
self.x = str(x)
D(1) # OK
D() # error: [missing-argument]
dataclass(...)A call like dataclass(order=True) returns a callable itself, which is then used as the decorator.
We can store the callable in a variable and later use it as a decorator:
from dataclasses import dataclass
dataclass_with_order = dataclass(order=True)
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclass-like function>
@dataclass_with_order
class C:
x: int
C(1) < C(2) # ok
invalid_order = dataclass(order=True, eq=False) # error: [invalid-dataclass] "`order=True` requires `eq=True`"
@invalid_order
class IndirectDecorator: ...
dataclass as a functionCalling dataclass with a class returns a class with a generated constructor:
from dataclasses import dataclass
class Point:
x: int
dataclass(Point)() # error: [missing-argument]
dataclass(Point)("one") # error: [invalid-argument-type]
reveal_type(dataclass(Point)(1).x) # revealed: int
Options can be passed in the same call:
class Ordered:
x: int
ordered = dataclass(Ordered, order=True)
ordered(1) < ordered(2)
ordered("one") # error: [invalid-argument-type]
class InvalidDirectApplication: ...
dataclass(InvalidDirectApplication, order=True, eq=False) # error: [invalid-dataclass] "`order=True` requires `eq=True`"
Passing None explicitly returns a decorator that uses the supplied options:
class Item:
x: int
ordered_item = dataclass(None, order=True)(Item)
reveal_type(ordered_item) # revealed: <class 'Item'>
ordered_item(1) < ordered_item(2)
The dataclass decorator returns the class itself. This means that the type of Person is type,
and attributes like the MRO are unchanged:
from dataclasses import dataclass
from ty_extensions._internal import reveal_mro
@dataclass
class Person:
name: str
age: int | None = None
reveal_type(type(Person)) # revealed: <class 'type'>
reveal_type(Person.__mro__) # revealed: tuple[type, ...]
reveal_mro(Person) # revealed: (<class 'Person'>, <class 'object'>)
The generated methods have the following signatures:
reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int | None = None) -> None
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool
Here, we make sure that the synthesized methods of dataclasses behave like proper functions.
[environment]
python-version = "3.12"
from dataclasses import dataclass
from typing import Callable
from types import FunctionType
from ty_extensions import static_assert
from ty_extensions._internal import CallableTypeOf, TypeOf, is_subtype_of, is_assignable_to, is_equivalent_to
@dataclass(order=True)
class C:
x: int
reveal_type(C.__init__) # revealed: (self: C, x: int) -> None
reveal_type(type(C.__init__)) # revealed: <class 'FunctionType'>
# We can access attributes that are defined on functions:
reveal_type(type(C.__init__).__code__) # revealed: CodeType
reveal_type(C.__init__.__code__) # revealed: CodeType
def equivalent_signature(self: C, x: int) -> None:
pass
type DunderInitType = TypeOf[C.__init__]
type EquivalentPureCallableType = Callable[[C, int], None]
type EquivalentFunctionLikeCallableType = CallableTypeOf[equivalent_signature]
static_assert(is_subtype_of(DunderInitType, EquivalentPureCallableType))
static_assert(is_assignable_to(DunderInitType, EquivalentPureCallableType))
static_assert(not is_subtype_of(EquivalentPureCallableType, DunderInitType))
static_assert(not is_assignable_to(EquivalentPureCallableType, DunderInitType))
static_assert(is_subtype_of(DunderInitType, EquivalentFunctionLikeCallableType))
static_assert(is_assignable_to(DunderInitType, EquivalentFunctionLikeCallableType))
static_assert(is_subtype_of(EquivalentFunctionLikeCallableType, DunderInitType))
static_assert(is_assignable_to(EquivalentFunctionLikeCallableType, DunderInitType))
static_assert(is_equivalent_to(EquivalentFunctionLikeCallableType, DunderInitType))
static_assert(is_subtype_of(DunderInitType, FunctionType))
It should be possible to mock out synthesized methods:
from unittest.mock import Mock
def test_c():
c = C(1)
c.__lt__ = Mock()
dataclasses.dataclassWhile we do not currently recognize the special behaviour of dataclasses.dataclass if it is called
imperatively, we recognize that it can be called imperatively and do not emit any false-positive
diagnostics on such calls:
from dataclasses import dataclass
from typing_extensions import TypeVar, dataclass_transform
U = TypeVar("U")
@dataclass_transform(kw_only_default=True)
def sequence(cls: type[U]) -> type[U]:
d = dataclass(
repr=False,
eq=False,
match_args=False,
kw_only=True,
)(cls)
reveal_type(d) # revealed: type[U@sequence] & Any
return d
@dataclass_transform(kw_only_default=True)
def sequence2(cls: type) -> type:
d = dataclass(
repr=False,
eq=False,
match_args=False,
kw_only=True,
)(cls)
reveal_type(d) # revealed: type & Any
return d
@dataclass_transform(kw_only_default=True)
def sequence3(cls: type[U]) -> type[U]:
# TODO: should reveal `type[U@sequence3]`
return reveal_type(dataclass(cls)) # revealed: Unknown
@dataclass_transform(kw_only_default=True)
def sequence4(cls: type) -> type:
# TODO: should reveal `type`
return reveal_type(dataclass(cls)) # revealed: Unknown
class Foo: ...
ordered_foo = dataclass(order=True)(Foo)
reveal_type(ordered_foo) # revealed: <class 'Foo'>
reveal_type(ordered_foo()) # revealed: Foo
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
Dynamic classes created with type() can be wrapped with dataclass() as a function:
from dataclasses import dataclass
# Basic dynamic class wrapped with dataclass
DynamicFoo = type("DynamicFoo", (), {})
DynamicFoo = dataclass(DynamicFoo)
# The class is recognized as a dataclass
reveal_type(DynamicFoo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
# Can create instances
instance = DynamicFoo()
reveal_type(instance) # revealed: DynamicFoo
Dynamic classes that inherit from a dataclass base also work:
from dataclasses import dataclass
@dataclass
class Base:
x: int
# Dynamic class inheriting from a dataclass
DynamicChild = type("DynamicChild", (Base,), {})
DynamicChild = dataclass(DynamicChild)
reveal_type(DynamicChild.__dataclass_fields__) # revealed: dict[str, Field[Any]]
@dataclass applicationsApplying @dataclass to a namedtuple class created via the functional form is problematic because
the namedtuple machinery conflicts with dataclass semantics:
from collections import namedtuple
from dataclasses import dataclass
NT = namedtuple("NT", "x y")
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass(NT)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass()(NT)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass(namedtuple("Inline1", "a b"))
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass()(namedtuple("Inline2", "a b"))
The same applies to typing.NamedTuple used in functional form:
from dataclasses import dataclass
from typing import NamedTuple
TNT = NamedTuple("TNT", [("x", int), ("y", int)])
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass(TNT)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass()(TNT)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass(NamedTuple("Inline1", [("a", str)]))
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass()(NamedTuple("Inline2", [("a", str)]))
Applying @dataclass to a functional Enum class should also be detected:
from dataclasses import dataclass
from enum import Enum
E = Enum("E", "A B C")
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass(E)
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass()(E)
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass(Enum("Inline1", "X Y"))
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass()(Enum("Inline2", "X Y"))
Applying @dataclass to a functional TypedDict class should also be detected:
from dataclasses import dataclass
from typing import TypedDict
TD = TypedDict("TD", {"x": int})
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `TypedDict` class"
dataclass(TD)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `TypedDict` class"
dataclass()(TD)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `TypedDict` class"
dataclass(TypedDict("Inline1", {"a": str}))
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `TypedDict` class"
dataclass()(TypedDict("Inline2", {"a": str}))
Applying @dataclass to an enum class is
explicitly not supported:
from dataclasses import dataclass
from enum import Enum
@dataclass
# error: [invalid-dataclass] "Enum class `Color` cannot be decorated with `@dataclass`"
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
This also applies to classes that inherit from an enum class:
from dataclasses import dataclass
from enum import Enum
class BaseColor(Enum):
def fancy_mixin_method(self) -> str:
return "hi"
@dataclass
# error: [invalid-dataclass]
class Color(BaseColor):
RED = 1
GREEN = 2
BLUE = 3
Applying @dataclass to a protocol class is invalid because protocols define abstract interfaces
and cannot be instantiated:
from dataclasses import dataclass
from typing import Protocol
@dataclass
# error: [invalid-dataclass] "Protocol class `Greeter` cannot be decorated with `@dataclass`"
class Greeter(Protocol):
def greet(self) -> str: ...
This also applies to classes that extend a protocol while remaining a protocol themselves:
from dataclasses import dataclass
from typing import Protocol
class BaseProtocol(Protocol):
def method(self) -> None: ...
@dataclass
# error: [invalid-dataclass]
class ExtendedProtocol(BaseProtocol, Protocol):
def other_method(self) -> None: ...
However, concrete classes that implement a protocol (without inheriting from Protocol directly)
can be decorated with @dataclass:
from dataclasses import dataclass
from typing import Protocol
class Greetable(Protocol):
name: str
def greet(self) -> str: ...
@dataclass
class Person(Greetable):
name: str
def greet(self) -> str:
return f"Hello, {self.name}!"
reveal_type(Person) # revealed: <class 'Person'>
dataclass() as a functionThe same restrictions apply when using dataclass() as a function call instead of a decorator:
from dataclasses import dataclass
from typing import NamedTuple, TypedDict, Protocol
from enum import Enum
class MyTuple(NamedTuple):
x: int
class MyDict(TypedDict):
x: int
class MyEnum(Enum):
A = 1
class MyProtocol(Protocol):
def method(self) -> None: ...
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass(MyTuple)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `NamedTuple` class"
dataclass()(MyTuple)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `TypedDict` class"
dataclass(MyDict)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a `TypedDict` class"
dataclass()(MyDict)
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass(MyEnum)
# error: [invalid-dataclass] "Cannot use `dataclass()` on an enum class"
dataclass()(MyEnum)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a protocol class"
dataclass(MyProtocol)
# error: [invalid-dataclass] "Cannot use `dataclass()` on a protocol class"
dataclass()(MyProtocol)