Asymmetric descriptor type apparently overriden when descriptor is set within the class body #11618
|
This could be a bug, or perhaps I don't understand how to correctly type my descriptors yet. The example below is a descriptor that is set with a float and get returns an int. If I only set the descriptor (with a float) outside the class body it works, and if I call the descriptor set method directly it also works, but if the descriptor is set within a method of the class body it adds this "| float" to the types we get from the descriptor. Please could someone point me in the right direction? from typing import (
Any,
assert_type,
Optional,
overload,
Self,
)
class Descriptor:
@overload
def __get__(self, instance: None, owner: type) -> Self:
...
@overload
def __get__(self, instance: Any, owner: type) -> int:
...
def __get__(self, instance: Optional[Any], owner: type) -> Self | int:
if instance is None:
return self
return 1
def __set__(self, instance: Any, value: float) -> None:
pass
class Test:
value1 = Descriptor()
value2 = Descriptor()
value3 = Descriptor()
value4 = Descriptor()
def __init__(self):
self.value1 = 1.1
self.set_value3()
type(self).value4.__set__(self, 1.4)
def set_value3(self):
self.value3 = 1.3
test = Test()
test.value2 = 1.2
assert_type(Test.value1, Descriptor)
# pyright says "Descriptor | float"
assert_type(test.value1, int)
# pyright says "int | float"
assert_type(test.value2, int)
# pyright says "int"
assert_type(test.value3, int)
# pyright says "int | float"
assert_type(test.value4, int)
# pyright says "int" |
Replies: 1 comment 1 reply
|
The behavior is reproducible, and the descriptor setter is being checked. The widening comes from a second static inference path: an unannotated assignment through Pyright then combines the class-body declaration That explains all three results:
The smallest fix is to make the class members explicitly typed descriptors: class Test:
value1: Descriptor = Descriptor()
value2: Descriptor = Descriptor()
value3: Descriptor = Descriptor()
value4: Descriptor = Descriptor()I verified the original reproducer with the current Pyright CLI: it reports the same three If you prefer not to add annotations, calling |
The behavior is reproducible, and the descriptor setter is being checked. The widening comes from a second static inference path: an unannotated assignment through
self.value1is also recorded as an instance-member declaration forvalue1.Pyright then combines the class-body declaration
value1 = Descriptor()with the inferredfloatdeclaration fromself.value1 = 1.1. Its evaluator documents that explicit declarations take precedence, while inferred declarations are combined into a union (declared types take precedence; inferred declarations are combined). The member-assignment path still invokes the descriptor protocol and caches the setter assignment (assignment path), but the inferred i…