pythonpython-typingpyrightpython-descriptors

How to create different type for class variable and instance variable


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.


Solution

  • 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