Understanding Python Descriptors: Definition, Types, and Priority
Descriptor Definition
A descriptor is a class that implements at least one of the methods __get__(), __set__(), or __delete__(). Descriptors are used to proxy attributes of another class. It is important to note that descriptors cannot be defined in the constructor of the class that uses them; they must be defined as class attributes. They belong to the class, not to instances. We can verify this by inspecting the dictionaries of both the instance and the class.
Descriptors are the underlying data structure implementation for many Python class features. Commonly used decorators like @classmethod, @staticmethod, @property, and even __slots__ are implemanted via descriptors. They are an important tool for many advanced libraries and frameworks, and a key component in large frameworks that use decorators or metaclasses. (Note: Decorators and metaclasses will be explained in future articles.)
Here is a example of a descriptor and a class that uses it:
class Descriptor:
def __init__(self, key, value_type):
self.key = key
self.value_type = value_type
def __get__(self, instance, owner):
print("Executing Descriptor's __get__")
return instance.__dict__[self.key]
def __set__(self, instance, value):
print("Executing Descriptor's __set__")
if not isinstance(value, self.value_type):
raise TypeError(f"Parameter {self.key} must be of type {self.value_type}")
instance.__dict__[self.key] = value
def __delete__(self, instance):
print("Executing Descriptor's __delete__")
instance.__dict__.pop(self.key)
class Person:
name = Descriptor("name", str)
age = Descriptor("age", int)
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("xiaoming", 15)
print(person.__dict__)
person.name
person.name = "jone"
print(person.__dict__)
In this example, the class Descriptor is a descriptor, and Person is the class that uses it.
The __dict__ attribute of a class is a built-in property that stores the class's static methods, class methods, regular functions, global variables, and some built-in attributes.
When accessing a descriptor variable, the __get__ method of the descriptor is called. When setting a descriptor variable, the __set__ method is called.
The output of the example above:

Types and Priority of Descriptors
Descriptors can be classified into two types: data descriptors and non-data descriptors.
- A data descriptor implements both
__set__()and__get__()methods. - A non-data descriptor implements only
__get__()(or__delete__()) but not__set__().
The priority order for attribute lookup in Python class instances is:
- Class attributes (highest)
- Data descriptors
- Instance attributes
- Non-data descriptors
__getattr__()if the attribute is not found (lowest)
In the descriptor example in the previous section, the instance attribute of person has a lower priority than the data descriptor Descriptor. Therefore, both assignment and retrieval invoke the descriptor's methods.
The next example demonstrates that class attributes have higher priority than data descriptors:
class Descriptor:
def __get__(self, instance, owner):
print("Executing Descriptor's __get__")
def __set__(self, instance, value):
print("Executing Descriptor's __set__")
def __delete__(self, instance):
print("Executing Descriptor's __delete__")
class University:
name = Descriptor()
def __init__(self, name):
self.name = name
University.name
University.name = "Shenzhen University"
print(University.name)
my_university = University("Xiamen University")
my_university.name
print(my_university.name)
In this example, the University class attribute name is assigned a value. Because class attributes have the highest priority, that value is always used. The output is:

What would the output be if we delete the line University.name = "Shenzhen University"?

Why is the instance's name attribute None? Because the instance attribute has lower priority than the descriptor, the descriptor's __get__ is called. Since the descriptor has no stored value, it returns None. Therefore, even though the instance has its own attribute, it cannot be retrieved proper because the descriptor intercepts the access.
The priority order of descriptors can be challenging to understand. It is recommended to write small test examples and observe the output to grasp the concept.
Reference: Original article