Skip to content

class-level descriptor access has incorrect type when descriptor set on instance #11382

@gdchinacat

Description

@gdchinacat

I have a descriptor class that evaluates to itself when accessed on the class and a value when accessed on an instance. The descriptor has a getitem implementation to allow class access to the field to be indexed. When doing this I get an error saying the type of instance value does not have this method. The descriptor get method is overloaded to indicate class access returns the descriptor and instance access the generic type of the descriptor. I believe the error is incorrect in this situation since pyright has enough information to know get will return the descriptor class and not the generic type. I have been using mypy and it correctly handles this case so I believe it is an issue with pyright and not the code it is checking.
While creating this minimal reproduction I noticed that the error only occurs when the initializer for the class with the descriptor field actually sets the value for the descriptor. If the initializer does not set the value the
type of the class level descriptor field access is revealed to be the descriptor type and not a union of that and the descriptor generic type.

from __future__ import annotations
from typing import Self, overload, reveal_type

class Descriptor[T]:
    def __init__(self, attr: str, default: T) -> None:
        self.__attr = attr
        self.default = default

    # The presence of these overloads has no bearing on pyright behavior, but mypy
    # will report a similar error to the one reported in this bug if they are omitted. I
    # believe they are necessary to properly determine whether Descriptor[int] or int
    # is returned based on whether the descriptor is accessed on a class or instance.
    @overload
    def __get__(self, instance: None, owner: type) -> Self: ...

    @overload
    def __get__(self, instance: object, owner: type) -> T: ...

    def __get__(self, instance: object|None, owner: type) -> T|Self:
        if instance is None:
            return self
        return getattr(instance, self.__attr, self.default)

    def __set__(self, instance: object, value: T) -> None:
        setattr(instance, self.__attr, value)

    def __getitem__(self, instance: object) -> object:
        return instance


class Foo:
    field = Descriptor[int]('_field', 0)
    def __init__(self, field_value: int) -> None:
        self.field = field_value

foo = Foo(0)
Foo.field[foo]  # "__getitem__" method not defined on "int"
reveal_type(Foo.field)  # Descriptor[int] | int  (should be Descriptor[int])

# The code below works as expected when the initializer doesn't assign a
# value to the descriptor. The type of accessing the descriptor on the class
# is correct and no error is emitted.

class Bar:
    field = Descriptor[int]('_field', 0)
    def __init__(self, field_value: int) -> None:
        pass

bar = Bar(0)
Bar.field[bar]  # no error
reveal_type(Bar.field)  # Descriptor[int]

This occurs when running pyright 1.1.408 from command line using python 3.14 on Linux.
This was discovered when I evaluated pyright against https://github.com/gdchinacat/reactions .

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions