crates/ty_python_semantic/resources/mdtest/properties.md
property is a built-in class in Python that can be used to model class attributes with custom
getters, setters, and deleters.
property is typically used as a decorator on a getter method. It turns the method into a property
object. When accessing the property on an instance, the descriptor protocol is invoked, which calls
the getter method:
class C:
@property
def my_property(self) -> int:
return 1
reveal_type(C().my_property) # revealed: int
When a property is accessed on the class directly, the descriptor protocol is also invoked, but
property.__get__ simply returns itself in this case (when instance is None):
reveal_type(C.my_property) # revealed: property
A property can also have a setter method, which is used to set the value of the property. The setter
method is defined using the @<property_name>.setter decorator. The setter method takes the value
to be set as an argument.
class C:
@property
def my_property(self) -> int:
return 1
@my_property.setter
def my_property(self, value: int) -> None:
pass
c = C()
reveal_type(c.my_property) # revealed: int
c.my_property = 2
# error: [invalid-assignment]
c.my_property = "a"
SelfA property that returns Self refers to an instance of the class:
from typing_extensions import Self
class Path:
@property
def parent(self) -> Self:
raise NotImplementedError
reveal_type(Path().parent) # revealed: Path
This also works when a setter is defined:
class Node:
@property
def parent(self) -> Self:
raise NotImplementedError
@parent.setter
def parent(self, value: Self) -> None:
pass
root = Node()
child = Node()
child.parent = root
reveal_type(child.parent) # revealed: Node
property.getterproperty.getter can be used to overwrite the getter method of a property. This does not overwrite
the existing setter:
class C:
@property
def my_property(self) -> int:
return 1
@my_property.setter
def my_property(self, value: int) -> None:
pass
@my_property.getter
def my_property(self) -> str:
return "a"
c = C()
reveal_type(c.my_property) # revealed: str
c.my_property = 2
# error: [invalid-assignment]
c.my_property = "b"
property.deleterWe support property.deleter, and it preserves the getter and setter:
class C:
@property
def my_property(self) -> int:
return 1
@my_property.setter
def my_property(self, value: int) -> None:
pass
@my_property.deleter
def my_property(self) -> None:
pass
c = C()
reveal_type(c.my_property) # revealed: int
del c.my_property
c.my_property = 2
# error: [invalid-assignment]
c.my_property = "a"
Direct property.__set__ and property.__delete__ calls return None for ordinary accessors, but
preserve Never/NoReturn for typed non-returning accessors:
from typing import Any, NoReturn, cast
def raw_setter(obj: object, value: object) -> int:
return 1
def raw_deleter(obj: object) -> int:
return 1
prop = property(fset=cast(Any, raw_setter), fdel=cast(Any, raw_deleter))
reveal_type(prop.__set__(object(), object())) # revealed: None
reveal_type(property.__set__(prop, object(), object())) # revealed: None
reveal_type(prop.__delete__(object())) # revealed: None
reveal_type(property.__delete__(prop, object())) # revealed: None
class NoReturnSetterAndDeleter:
@property
def x(self) -> int:
return 1
@x.setter
def x(self, value: int) -> NoReturn:
raise RuntimeError
@x.deleter
def x(self) -> NoReturn:
raise RuntimeError
def direct_set() -> None:
reveal_type(NoReturnSetterAndDeleter.x.__set__(NoReturnSetterAndDeleter(), 1)) # revealed: Never
def direct_set_unbound() -> None:
cls = NoReturnSetterAndDeleter
reveal_type(type(cls.x).__set__(cls.x, cls(), 1)) # revealed: Never
def direct_delete() -> None:
reveal_type(NoReturnSetterAndDeleter.x.__delete__(NoReturnSetterAndDeleter())) # revealed: Never
def direct_delete_unbound() -> None:
cls = NoReturnSetterAndDeleter
reveal_type(type(cls.x).__delete__(cls.x, cls())) # revealed: Never
Distinct property definitions in statically unknown class-body branches should remain distinct, the same way methods do:
from random import random
class Baz:
if random():
def method(self) -> int:
return 42
@property
def prop(self) -> int:
return 42
else:
def method(self) -> str:
return "hello"
@property
def prop(self) -> str:
return "hello"
baz = Baz()
reveal_type(baz.prop) # revealed: int | str
reveal_type(baz.method()) # revealed: int | str
When attempting to write to a read-only property, we emit an error:
class C:
@property
def attr(self) -> int:
return 1
c = C()
# error: [invalid-assignment]
c.attr = 2
When attempting to read a write-only property, we emit an error:
class C:
def attr_setter(self, value: int) -> None:
pass
attr = property(fset=attr_setter)
c = C()
c.attr = 1
# TODO: An error should be emitted here.
# See https://github.com/astral-sh/ruff/issues/16298 for more details.
reveal_type(c.attr) # revealed: Never
from typing import NoReturn
class NoReturnSetter:
@property
def x(self) -> int:
return 1
@x.setter
def x(self, value: int) -> NoReturn:
raise RuntimeError
no_return_setter = NoReturnSetter()
# error: [invalid-assignment] "Cannot assign to attribute `x` on type `NoReturnSetter` whose `__set__` method returns `Never`/`NoReturn`"
no_return_setter.x = 1
class C:
@property
def attr(self) -> int:
return 1
# error: [invalid-argument-type] "Argument to bound method `property.setter` is incorrect: Expected `(Any, Any, /) -> None`, found `def attr(self) -> None`"
@attr.setter
def attr(self) -> None:
pass
class C:
# error: [invalid-argument-type] "Argument to class `property` is incorrect: Expected `((Any, /) -> Any) | None`, found `def attr(self, x: int) -> int`"
@property
def attr(self, x: int) -> int:
return 1
class C:
@property
def attr(self) -> int:
return 1
c = C()
del c.attr # snapshot
error[invalid-assignment]: Cannot delete read-only property `attr` on object of type `C`
--> src/mdtest_snippet.py:7:5
|
7 | del c.attr # snapshot
| ^^^^^^ Attempted deletion of `C.attr` here
|
::: src/mdtest_snippet.py:3:9
|
3 | def attr(self) -> int:
| ---- Property `C.attr` defined here with no deleter
|
Properties can also be constructed manually using the property class. We support this:
class C:
def attr_getter(self) -> int:
return 1
attr = property(attr_getter)
c = C()
reveal_type(c.attr) # revealed: int
If we try to declare attr as property, we fail to understand the property, since the property
declaration shadows the more precise type that we infer for property(attr_getter) (which includes
the actual information about the getter).
class C:
def attr_getter(self) -> int:
return 1
attr: property = property(attr_getter)
c = C()
reveal_type(c.attr) # revealed: Unknown
We should emit an error when trying to set an attribute that was created using a manually
constructed property with fset=None, just like we do for decorator-based read-only properties:
class Foo:
myprop = property(fget=lambda self: 42, fset=None)
class Bar:
@property
def myprop(self) -> int:
return 42
f = Foo()
# error: [invalid-assignment]
f.myprop = 56
b = Bar()
# error: [invalid-assignment]
b.myprop = 42
In this section, we trace through some of the steps that make properties work. We start with a
simple class C and a property attr:
class C:
def __init__(self):
self._attr: int = 0
@property
def attr(self) -> int:
return self._attr
@attr.setter
def attr(self, value: str) -> None:
self._attr = len(value)
Next, we create an instance of C. As we have seen above, accessing attr on the instance will
return an int:
c = C()
reveal_type(c.attr) # revealed: int
Behind the scenes, when we write c.attr, the first thing that happens is that we statically look
up the symbol attr on the meta-type of c, i.e. the class C. We can emulate this static lookup
using inspect.getattr_static, to see that attr is actually an instance of the property class:
from inspect import getattr_static
attr_property = getattr_static(C, "attr")
reveal_type(attr_property) # revealed: property
The property class has a __get__ method, which makes it a descriptor. It also has a __set__
method, which means that it is a data descriptor (if there is no setter, __set__ is still
available but yields an AttributeError at runtime).
reveal_type(type(attr_property).__get__) # revealed: <wrapper-descriptor '__get__' of 'property' objects>
reveal_type(type(attr_property).__set__) # revealed: <wrapper-descriptor '__set__' of 'property' objects>
When we access c.attr, the __get__ method of the property class is called, passing the
property object itself as the first argument, and the class instance c as the second argument. The
third argument is the "owner" which can be set to None or to C in this case:
reveal_type(type(attr_property).__get__(attr_property, c, C)) # revealed: int
reveal_type(type(attr_property).__get__(attr_property, c, None)) # revealed: int
Alternatively, the above can also be written as a method call:
reveal_type(attr_property.__get__(c, C)) # revealed: int
When we access attr on the class itself, the descriptor protocol is also invoked, but the instance
argument is set to None. When instance is None, the call to property.__get__ returns the
property instance itself. So the following expressions are all equivalent
reveal_type(attr_property) # revealed: property
reveal_type(C.attr) # revealed: property
reveal_type(attr_property.__get__(None, C)) # revealed: property
reveal_type(type(attr_property).__get__(attr_property, None, C)) # revealed: property
When we set the property using c.attr = "a", the __set__ method of the property class is called.
This attribute access desugars to
type(attr_property).__set__(attr_property, c, "a")
# error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed"
type(attr_property).__set__(attr_property, c, 1)
which is also equivalent to the following expressions:
attr_property.__set__(c, "a")
# error: [call-non-callable]
attr_property.__set__(c, 1)
C.attr.__set__(c, "a")
# error: [call-non-callable]
C.attr.__set__(c, 1)
Properties also have fget and fset attributes that can be used to retrieve the original getter
and setter functions, respectively.
reveal_type(attr_property.fget) # revealed: def attr(self) -> int
reveal_type(attr_property.fget(c)) # revealed: int
reveal_type(attr_property.fset) # revealed: def attr(self, value: str) -> None
reveal_type(attr_property.fset(c, "a")) # revealed: None
# error: [invalid-argument-type]
attr_property.fset(c, 1)
At runtime, attr_property.__get__ and attr_property.__set__ are both instances of
types.MethodWrapperType:
import types
from ty_extensions import TypeOf, static_assert, is_subtype_of
static_assert(is_subtype_of(TypeOf[attr_property.__get__], types.MethodWrapperType))
static_assert(is_subtype_of(TypeOf[attr_property.__set__], types.MethodWrapperType))
static_assert(not is_subtype_of(TypeOf[attr_property.__get__], types.WrapperDescriptorType))
static_assert(not is_subtype_of(TypeOf[attr_property.__set__], types.WrapperDescriptorType))
static_assert(not is_subtype_of(TypeOf[attr_property.__get__], types.BuiltinMethodType))
static_assert(not is_subtype_of(TypeOf[attr_property.__set__], types.BuiltinMethodType))
Property equivalence and disjointness are structural over the getter and setter types. We use
standalone property objects here so TypeOf[...] sees the raw property type rather than the
Unknown | ... that can arise from class-attribute lookup. For the subtype cases, we construct
properties through helper functions with Callable-typed parameters so the slot types are
structural rather than exact function literals:
from typing import Callable
from ty_extensions import (
CallableTypeOf,
TypeOf,
is_assignable_to,
is_disjoint_from,
is_equivalent_to,
is_subtype_of,
static_assert,
)
def get_int(self) -> int:
return 1
def get_str(self) -> str:
return "a"
def set_int(self, value: int) -> None:
pass
def set_object(self, value: object) -> None:
pass
def set_str(self, value: str) -> None:
pass
def get_equiv_a(self, /) -> int:
return 1
def get_equiv_b(other, /) -> int:
return 1
GetterReturnsInt = Callable[[object], int]
GetterReturnsObject = Callable[[object], object]
SetterAcceptsInt = Callable[[object, int], None]
SetterAcceptsObject = Callable[[object, object], None]
# Use `CallableTypeOf[...]` here rather than plain `Callable[...]` so these getters remain
# equivalent as types while still carrying distinct callable metadata and distinct Salsa IDs.
def assert_equivalent_properties(
getter_a: CallableTypeOf[get_equiv_a],
getter_b: CallableTypeOf[get_equiv_b],
):
getter_only_equivalent_a = property(getter_a)
getter_only_equivalent_b = property(getter_b)
static_assert(is_equivalent_to(TypeOf[getter_only_equivalent_a], TypeOf[getter_only_equivalent_b]))
static_assert(not is_disjoint_from(TypeOf[getter_only_equivalent_a], TypeOf[getter_only_equivalent_b]))
def assert_structural_property_relations(
getter_sub: GetterReturnsInt,
getter_super: GetterReturnsObject,
setter_sub: SetterAcceptsObject,
setter_super: SetterAcceptsInt,
):
getter_covariant_sub = property(getter_sub)
getter_covariant_super = property(getter_super)
setter_contravariant_sub = property(fset=setter_sub)
setter_contravariant_super = property(fset=setter_super)
both_structural_sub = property(getter_sub, setter_sub)
both_structural_super = property(getter_super, setter_super)
static_assert(not is_equivalent_to(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super]))
static_assert(not is_equivalent_to(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super]))
static_assert(not is_equivalent_to(TypeOf[both_structural_sub], TypeOf[both_structural_super]))
static_assert(is_subtype_of(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super]))
static_assert(not is_subtype_of(TypeOf[getter_covariant_super], TypeOf[getter_covariant_sub]))
static_assert(is_assignable_to(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super]))
static_assert(not is_assignable_to(TypeOf[getter_covariant_super], TypeOf[getter_covariant_sub]))
static_assert(is_subtype_of(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super]))
static_assert(not is_subtype_of(TypeOf[setter_contravariant_super], TypeOf[setter_contravariant_sub]))
static_assert(is_assignable_to(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super]))
static_assert(not is_assignable_to(TypeOf[setter_contravariant_super], TypeOf[setter_contravariant_sub]))
static_assert(is_subtype_of(TypeOf[both_structural_sub], TypeOf[both_structural_super]))
static_assert(not is_subtype_of(TypeOf[both_structural_super], TypeOf[both_structural_sub]))
static_assert(is_assignable_to(TypeOf[both_structural_sub], TypeOf[both_structural_super]))
static_assert(not is_assignable_to(TypeOf[both_structural_super], TypeOf[both_structural_sub]))
static_assert(is_subtype_of(TypeOf[both_structural_sub.__get__], TypeOf[both_structural_super.__get__]))
static_assert(not is_subtype_of(TypeOf[both_structural_super.__get__], TypeOf[both_structural_sub.__get__]))
static_assert(is_subtype_of(TypeOf[both_structural_sub.__set__], TypeOf[both_structural_super.__set__]))
static_assert(not is_subtype_of(TypeOf[both_structural_super.__set__], TypeOf[both_structural_sub.__set__]))
static_assert(not is_disjoint_from(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super]))
static_assert(not is_disjoint_from(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super]))
static_assert(not is_disjoint_from(TypeOf[both_structural_sub], TypeOf[both_structural_super]))
static_assert(not is_disjoint_from(TypeOf[both_structural_sub.__get__], TypeOf[both_structural_super.__get__]))
static_assert(not is_disjoint_from(TypeOf[both_structural_sub.__set__], TypeOf[both_structural_super.__set__]))
empty_a = property()
empty_b = property()
getter_only_a = property(get_int)
getter_only_b = property(get_int)
getter_only_c = property(get_str)
getter_only_d = property(get_int, None)
setter_only_a = property(fset=set_int)
setter_only_b = property(fset=set_int)
setter_only_c = property(fset=set_str)
setter_only_d = property(None, set_int)
both_a = property(get_int, set_int)
both_b = property(get_int, set_int)
both_c = property(get_int, set_str)
both_d = property(get_str, set_int)
static_assert(is_equivalent_to(TypeOf[empty_a], TypeOf[empty_b]))
static_assert(is_equivalent_to(TypeOf[getter_only_a], TypeOf[getter_only_b]))
static_assert(is_equivalent_to(TypeOf[getter_only_a], TypeOf[getter_only_d]))
static_assert(is_equivalent_to(TypeOf[setter_only_a], TypeOf[setter_only_b]))
static_assert(is_equivalent_to(TypeOf[setter_only_a], TypeOf[setter_only_d]))
static_assert(is_equivalent_to(TypeOf[both_a], TypeOf[both_b]))
static_assert(not is_equivalent_to(TypeOf[empty_a], TypeOf[getter_only_a]))
static_assert(not is_equivalent_to(TypeOf[empty_a], TypeOf[setter_only_a]))
static_assert(not is_equivalent_to(TypeOf[getter_only_a], TypeOf[getter_only_c]))
static_assert(not is_equivalent_to(TypeOf[getter_only_a], TypeOf[setter_only_a]))
static_assert(not is_equivalent_to(TypeOf[getter_only_a], TypeOf[both_a]))
static_assert(not is_equivalent_to(TypeOf[setter_only_a], TypeOf[setter_only_c]))
static_assert(not is_equivalent_to(TypeOf[setter_only_a], TypeOf[both_a]))
static_assert(not is_equivalent_to(TypeOf[both_a], TypeOf[both_c]))
static_assert(not is_equivalent_to(TypeOf[both_a], TypeOf[both_d]))
static_assert(not is_disjoint_from(TypeOf[empty_a], TypeOf[empty_b]))
static_assert(not is_disjoint_from(TypeOf[getter_only_a], TypeOf[getter_only_b]))
static_assert(not is_disjoint_from(TypeOf[getter_only_a], TypeOf[getter_only_d]))
static_assert(not is_disjoint_from(TypeOf[setter_only_a], TypeOf[setter_only_b]))
static_assert(not is_disjoint_from(TypeOf[setter_only_a], TypeOf[setter_only_d]))
static_assert(not is_disjoint_from(TypeOf[both_a], TypeOf[both_b]))
static_assert(is_disjoint_from(TypeOf[empty_a], TypeOf[getter_only_a]))
static_assert(is_disjoint_from(TypeOf[empty_a], TypeOf[setter_only_a]))
static_assert(is_disjoint_from(TypeOf[getter_only_a], TypeOf[getter_only_c]))
static_assert(is_disjoint_from(TypeOf[getter_only_a], TypeOf[setter_only_a]))
static_assert(is_disjoint_from(TypeOf[getter_only_a], TypeOf[both_a]))
static_assert(is_disjoint_from(TypeOf[setter_only_a], TypeOf[setter_only_c]))
static_assert(is_disjoint_from(TypeOf[setter_only_a], TypeOf[both_a]))
static_assert(is_disjoint_from(TypeOf[both_a], TypeOf[both_c]))
static_assert(is_disjoint_from(TypeOf[both_a], TypeOf[both_d]))
assert_equivalent_properties(get_equiv_a, get_equiv_b)
assert_structural_property_relations(get_int, get_int, set_object, set_object)