@@ -95,7 +95,7 @@ class DataDescriptor:
9595 def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" data" ]:
9696 return " data"
9797
98- def __set__ (self , instance : int , value ) -> None :
98+ def __set__ (self , instance : object , value : int ) -> None :
9999 pass
100100
101101class NonDataDescriptor :
@@ -133,42 +133,6 @@ reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
133133C.data_descriptor = " something else" # This is okay
134134```
135135
136- ## Descriptor protocol for class objects
137-
138- When a descriptor is accessed on a class object, the following precedence chain is used:
139-
140- - Data descriptor on the metaclass
141- - Data or non-data descriptor on the class
142- - Class attribute
143- - Non-data descriptor on the metaclass
144-
145- ``` py
146- from typing import Literal
147-
148- class DataDescriptor :
149- def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" data" ]:
150- return " data"
151-
152- def __set__ (self , instance : int , value ) -> None :
153- pass
154-
155- class NonDataDescriptor :
156- def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" non-data" ]:
157- return " non-data"
158-
159- class Meta (type ):
160- meta_data_descriptor: DataDescriptor = DataDescriptor()
161- meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
162-
163- class DescriptorsOnMetaClass (metaclass = Meta ):
164- # meta_data_descriptor = "shadowed?"
165- # meta_non_data_descriptor = "shadowed?"
166- ...
167-
168- reveal_type(DescriptorsOnMetaClass.meta_data_descriptor) # revealed: Literal["data"]
169- reveal_type(DescriptorsOnMetaClass.meta_non_data_descriptor) # revealed: Literal["non-data"]
170- ```
171-
172136## Possibly unbound descriptors, unions with other attributes
173137
174138``` py
@@ -230,6 +194,180 @@ def _(flag: bool):
230194 reveal_type(UnionWithInstanceAttribute().data) # revealed: int | str
231195```
232196
197+ ## Descriptor protocol for class objects
198+
199+ When attributes are accessed on a class object, the following [ precedence chain] is used:
200+
201+ - Data descriptor on the metaclass
202+ - Data or non-data descriptor on the class
203+ - Class attribute
204+ - Non-data descriptor on the metaclass
205+ - Metaclass attribute
206+
207+ To verify this, we define a data and a non-data descriptor:
208+
209+ ``` py
210+ from typing import Literal, Any
211+
212+ class DataDescriptor :
213+ def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" data" ]:
214+ return " data"
215+
216+ def __set__ (self , instance : object , value : str ) -> None :
217+ pass
218+
219+ class NonDataDescriptor :
220+ def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" non-data" ]:
221+ return " non-data"
222+ ```
223+
224+ First, we make sure that the descriptors are correctly accessed when defined on the metaclass or the
225+ class:
226+
227+ ``` py
228+ class Meta1 (type ):
229+ meta_data_descriptor: DataDescriptor = DataDescriptor()
230+ meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
231+
232+ class C1 (metaclass = Meta1 ):
233+ class_data_descriptor: DataDescriptor = DataDescriptor()
234+ class_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
235+
236+ reveal_type(C1.meta_data_descriptor) # revealed: Literal["data"]
237+ reveal_type(C1.meta_non_data_descriptor) # revealed: Literal["non-data"]
238+
239+ reveal_type(C1.class_data_descriptor) # revealed: Literal["data"]
240+ reveal_type(C1.class_non_data_descriptor) # revealed: Literal["non-data"]
241+ ```
242+
243+ Next, we demonstrate that a * metaclass data descriptor* takes precedence over all class-level
244+ attributes:
245+
246+ ``` py
247+ class Meta2 (type ):
248+ meta_data_descriptor1: DataDescriptor = DataDescriptor()
249+ meta_data_descriptor2: DataDescriptor = DataDescriptor()
250+
251+ class ClassLevelDataDescriptor :
252+ def __get__ (self , instance : object , owner : type | None = None ) -> Literal[" class level data descriptor" ]:
253+ return " class level data descriptor"
254+
255+ def __set__ (self , instance : object , value : str ) -> None :
256+ pass
257+
258+ class C2 (metaclass = Meta2 ):
259+ meta_data_descriptor1: Literal[" value on class" ] = " value on class"
260+ meta_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
261+
262+ reveal_type(C2.meta_data_descriptor1) # revealed: Literal["data"]
263+ reveal_type(C2.meta_data_descriptor2) # revealed: Literal["data"]
264+ ```
265+
266+ On the other hand, normal metaclass attributes and metaclass non-data descriptors are shadowed by
267+ class-level attributes (descriptor or not):
268+
269+ ``` py
270+ class Meta3 (type ):
271+ meta_attribute1: Literal[" value on metaclass" ] = " value on metaclass"
272+ meta_attribute2: Literal[" value on metaclass" ] = " value on metaclass"
273+ meta_non_data_descriptor1: NonDataDescriptor = NonDataDescriptor()
274+ meta_non_data_descriptor2: NonDataDescriptor = NonDataDescriptor()
275+
276+ class C3 (metaclass = Meta3 ):
277+ meta_attribute1: Literal[" value on class" ] = " value on class"
278+ meta_attribute2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
279+ meta_non_data_descriptor1: Literal[" value on class" ] = " value on class"
280+ meta_non_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
281+
282+ reveal_type(C3.meta_attribute1) # revealed: Literal["value on class"]
283+ reveal_type(C3.meta_attribute2) # revealed: Literal["class level data descriptor"]
284+ reveal_type(C3.meta_non_data_descriptor1) # revealed: Literal["value on class"]
285+ reveal_type(C3.meta_non_data_descriptor2) # revealed: Literal["class level data descriptor"]
286+ ```
287+
288+ Finally, metaclass attributes and metaclass non-data descriptors are only accessible when they are
289+ not shadowed by class-level attributes:
290+
291+ ``` py
292+ class Meta4 (type ):
293+ meta_attribute: Literal[" value on metaclass" ] = " value on metaclass"
294+ meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
295+
296+ class C4 (metaclass = Meta4 ): ...
297+
298+ reveal_type(C4.meta_attribute) # revealed: Literal["value on metaclass"]
299+ reveal_type(C4.meta_non_data_descriptor) # revealed: Literal["non-data"]
300+ ```
301+
302+ When a metaclass data descriptor is possibly unbound, we union the result type of its ` __get__ `
303+ method with an underlying class level attribute, if present:
304+
305+ ``` py
306+ def _ (flag : bool ):
307+ class Meta5 (type ):
308+ if flag:
309+ meta_data_descriptor1: DataDescriptor = DataDescriptor()
310+ meta_data_descriptor2: DataDescriptor = DataDescriptor()
311+
312+ class C5 (metaclass = Meta5 ):
313+ meta_data_descriptor1: Literal[" value on class" ] = " value on class"
314+
315+ reveal_type(C5.meta_data_descriptor1) # revealed: Literal["value on class", "data"]
316+ # error: [possibly-unbound-attribute]
317+ reveal_type(C5.meta_data_descriptor2) # revealed: Literal["data"]
318+ ```
319+
320+ When a class-level attribute is possibly unbound, we union its (descriptor protocol) type with the
321+ metaclass attribute (unless it's a data descriptor, which always takes precedence):
322+
323+ ``` py
324+ def _ (flag : bool ):
325+ class Meta6 (type ):
326+ attribute1: DataDescriptor = DataDescriptor()
327+ attribute2: NonDataDescriptor = NonDataDescriptor()
328+ attribute3: Literal[" value on metaclass" ] = " value on metaclass"
329+
330+ class C6 (metaclass = Meta6 ):
331+ if flag:
332+ attribute1: Literal[" value on class" ] = " value on class"
333+ attribute2: Literal[" value on class" ] = " value on class"
334+ attribute3: Literal[" value on class" ] = " value on class"
335+ attribute4: Literal[" value on class" ] = " value on class"
336+
337+ reveal_type(C6.attribute1) # revealed: Literal["data"]
338+ reveal_type(C6.attribute2) # revealed: Literal["value on class", "non-data"]
339+ reveal_type(C6.attribute3) # revealed: Literal["value on class", "value on metaclass"]
340+ # error: [possibly-unbound-attribute]
341+ reveal_type(C6.attribute4) # revealed: Literal["value on class"]
342+ ```
343+
344+ Finally, we can also have unions of various types of attributes:
345+
346+ ``` py
347+ def _ (flag : bool ):
348+ class Meta7 (type ):
349+ if flag:
350+ union_of_metaclass_attributes: Literal[1 ] = 1
351+ union_of_metaclass_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
352+ else :
353+ union_of_metaclass_attributes: Literal[2 ] = 2
354+ union_of_metaclass_data_descriptor_and_attribute: Literal[2 ] = 2
355+
356+ class C7 (metaclass = Meta7 ):
357+ if flag:
358+ union_of_class_attributes: Literal[1 ] = 1
359+ union_of_class_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
360+ else :
361+ union_of_class_attributes: Literal[2 ] = 2
362+ union_of_class_data_descriptor_and_attribute: Literal[2 ] = 2
363+
364+ reveal_type(C7.union_of_metaclass_attributes) # revealed: Literal[1, 2]
365+ # TODO : should be `Literal["data", 2]`
366+ reveal_type(C7.union_of_metaclass_data_descriptor_and_attribute) # revealed: Literal["data"]
367+ reveal_type(C7.union_of_class_attributes) # revealed: Literal[1, 2]
368+ reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
369+ ```
370+
233371## Built-in ` property ` descriptor
234372
235373The built-in ` property ` decorator creates a descriptor. The names for attribute reads/writes are
@@ -533,4 +671,5 @@ wrapper_descriptor(f, None, type(f), "one too many")
533671```
534672
535673[ descriptors ] : https://docs.python.org/3/howto/descriptor.html
674+ [ precedence chain ] : https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L5393-L5481
536675[ simple example ] : https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant
0 commit comments