class-level descriptor access has incorrect type when descriptor set on instance
- Dominant language
- Python
- Stars
- 15.6k
- Forks
- 1.8k
- Avg merge
- 12h 13m
- Merged PRs (30d)
- 52
Description
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.
```python
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 .
Contributor guide
Research direction
Start by running the minimal reproduction from the issue with pyright 1.1.408 and compare the reported type of Foo.field with the Bar case and mypy's result. Trace pyright's descriptor and generic type inference behavior, then add a regression test showing that class-level access remains Descriptor[int] when the instance initializer assigns the descriptor field.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100