2025, Nov 23 17:00
Type-safe Python descriptors in Pyright: fix dataclass initialization errors with __set__ and __get__ overloads
Fix Pyright dataclass type mismatches by making your descriptor a data-descriptor with __set__ and __set_name__, and overloading __get__ for class vs instance.
Making a Python descriptor look like one type on the class and another on the instance is a common need when you want clean ergonomics with static type checking. Overloading __get__ often gets you most of the way there, but Pyright will still complain during dataclass initialization if the descriptor isn’t a proper data-descriptor. The symptom looks like a type mismatch when passing an instance value in the constructor.
Problem
The goal is to have a class-level access that returns the descriptor object and an instance-level access that returns a concrete value. Overloading __get__ communicates this to the type checker, yet Pyright flags the dataclass initializer.
Literal[1] is not assignable to Field[int]
Reproducible example:
import typing as tp
from dataclasses import dataclass
class Cell[ValT]:
def __init__(self, seed: ValT):
self.seed = seed
self.prime = True
def __get__(self, obj, owner):
if self.prime:
self.prime = False
return self.seed
return self
if tp.TYPE_CHECKING:
@tp.overload
def __get__(self, obj: None, owner: type) -> tp.Self: ...
@tp.overload
def __get__(self, obj: object, owner: type) -> ValT: ...
@dataclass
class Record:
slot: Cell[int] = Cell(0)
if __name__ == "__main__":
cls_slot: Cell = Record.slot
inst_slot: int = Record().slot
assert isinstance(cls_slot, Cell)
assert isinstance(inst_slot, int)
item = Record(slot=1)
What’s going on
The overloaded __get__ describes different return types for class and instance access, but the attribute is not a data-descriptor. To be a data-descriptor, it needs a __set__ method. Without that, Pyright won’t treat assignments through the dataclass constructor as writes to the instance value and will keep seeing the attribute as the descriptor type, leading to the reported mismatch. The accepted value type is driven by the signature of __set__, which must accept the generic instance type.
Fix and working version
Implement a proper data-descriptor by adding __set__ and using __set_name__ to store the per-instance value under a private attribute. The instance value type is then enforced through the generic parameter in __set__. The existing first-use behavior is preserved.
import typing as tp
from dataclasses import dataclass
class Cell[ValT]:
def __init__(self, seed: ValT):
self.seed = seed
self.prime = True
@tp.overload
def __get__(self, obj: None, owner: type) -> tp.Self: ...
@tp.overload
def __get__(self, obj: object, owner: type) -> ValT: ...
def __get__(self, obj, owner):
if self.prime:
self.prime = False
return self.seed
if obj is None:
return self
return getattr(obj, self._alias, self.seed)
def __set_name__(self, owner, name):
self._alias = "_" + name
def __set__(self, obj, value: ValT):
setattr(obj, self._alias, value)
@dataclass
class Record:
slot: Cell[int] = Cell(0)
if __name__ == "__main__":
cls_slot = Record.slot
reveal_type(cls_slot) # Cell[int]
rec = Record()
inst_slot: int = rec.slot
reveal_type(inst_slot) # int
assert isinstance(cls_slot, Cell)
assert isinstance(inst_slot, int)
valid = Record(slot=1) # OK
invalid = Record(slot="1") # Error
Why this matters
Descriptors that act like different types on class and instance are powerful, but static type checkers need clear signals. Ensuring the descriptor is a data-descriptor with a __set__ that accepts the generic instance type aligns runtime behavior with static expectations, so constructor arguments and attribute writes type-check as intended.
Takeaways
If Pyright treats your descriptor-annotated dataclass field as the descriptor type during initialization, make it a data-descriptor. Provide a __set__ whose value parameter matches the instance’s generic type, and, if needed, use __set_name__ to manage per-instance storage. Keep your __get__ overloads to reflect class versus instance access, and the checker will accept constructor assignments and instance reads consistently.