I want to explain to Pyright that my variables in class and instance have different types.
I managed to overload __get__
method to achieve this, but now Pyright complains about initialization of instances (see last line):
Literal[1] is not assignable to Field[int]
My code:
import typing as ty
from dataclasses import dataclass
class Field[InstanceT]:
def __init__(self, default: InstanceT):
self.default = default
self.first = True
def __get__(self, obj, owner):
if self.first:
self.first = False
return self.default
return self
if ty.TYPE_CHECKING:
@ty.overload
def __get__(self, obj: None, owner: type) -> ty.Self: ...
@ty.overload
def __get__(self, obj: object, owner: type) -> InstanceT: ...
@dataclass
class Model:
field: Field[int] = Field(0)
if __name__ == "__main__":
# It`s fine
class_field: Field = Model.field
instance_field: int = Model().field
assert isinstance(class_field, Field)
assert isinstance(instance_field, int)
# Literal[1] is not assignable to field[int]
obj = Model(field=1)
Asserts are true, but Pyright complains.
You want to have a data-descriptor, such it needs a __set__
method. You will get an error depending on the signature of __set__
, you want it to accept your generic.
A working example could look like this, the instance value will be stored on the objects _field
attribute, see the __set_name__
magic, you could of course also store in the Field and not on the instance. I am not sure about your self.first
logic - so you might want to change some parts.
import typing as ty
from dataclasses import dataclass
class Field[InstanceT]:
def __init__(self, default: InstanceT):
self.default = default
self.first = True
@ty.overload
def __get__(self, obj: None, owner: type) -> ty.Self: ...
@ty.overload
def __get__(self, obj: object, owner: type) -> InstanceT: ...
def __get__(self, obj, owner):
if self.first:
self.first = False
return self.default
if obj is None: # <-- called on class
return self
return getattr(obj, self._name, self.default) # <-- called on instance
def __set_name__(self, owner, name):
self._name = "_" +name
def __set__(self, obj, value: InstanceT):
setattr(obj, self._name, value)
@dataclass
class Model:
field: Field[int] = Field(0)
if __name__ == "__main__":
class_field = Model.field
reveal_type(class_field) # Field[int]
model = Model()
instance_field: int = model.field
reveal_type(instance_field) # int
assert isinstance(class_field, Field)
assert isinstance(instance_field, int)
# Literal[1] is not assignable to field[int]
obj = Model(field=1) # OK
Model(field="1") # Error