What are descriptors in Python?

In Python, a descriptor is an attribute of an object with specialized behavior controlled by methods like get(), set(), and delete(). It is a class that allows us to modify the properties of another class object.

A descriptor implements at least one of the methods __get__()__set()__, or __delete()__, which define how an attribute is accessed, modified, and deleted when it is part of a class that uses a descriptor.

  • __get__() accesses the attribute or when you want to extract some information. It returns the value of the attribute or raises the AttributeError exception if a requested attribute is not present.

  • __set__() is called in an attribute assignment operation that sets the value of an attribute. Returns nothing. But can raise the AttributeError exception.

  • __delete__() controls a delete operation, i.e., when you want to delete the attribute from an object. Returns nothing.

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.

How to create a descriptor

Using property()

The property() function in Python is a built-in function that allows you to create a special kind of attribute known as a property; it is a feature of a class that looks like a regular attribute but is managed using methods, typically getter, setter, and deleter methods.

The property() function takes up to four parameters (in the given order):

  1. fget: It is a reference to the method that will be used as the getter. It is called when the property is accessed.

  2. fset: It is a reference to the method that will be used as the setter. It is called when the property is assigned a new value.

  3. fdel: It is a reference to the method that will be used as the deleter. It is called when the del statement is used to delete the property.

  4. doc: It is an optional parameter that allows you to set the docstring for the property.

class Circle:
def __init__(self, radius):
self._radius = radius
# Getter method to retrieve the radius
def get(self):
print('Get radius value')
return self._radius
# Setter method to set the radius
def set(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
print('Set radius value to', value)
self._radius = value
# Deleter method to delete the radius attribute
def delete(self):
print('Delete radius')
del self._radius
# Define a property using property() function
radius = property(get, set, delete)
# Create an instance of Circle
my_circle = Circle(5)
# Access the radius using the property
print('Radius:', my_circle.radius)
# Set a new radius using the property
my_circle.radius = 7
# Delete the radius attribute using the property
del my_circle.radius

In this example, the Circle class has a radius property that allows getting, setting, and deleting the radius. get() retrieves the radius, set() assigns a value to the radius (with a check for negative values), and delete() removes the radius attribute. At last, the property() function is used to define the radius property. Let's look at the code line by line:

  • Line 1: Defines a new class named Circle.

  • Line 2: Defines the initializer method (__init__) for the Circle class, which takes radius as an argument.

  • Line 3: Initializes the instance variable _radius with the value passed to the constructor.

  • Line 6: Defines a getter method get for retrieving the value of _radius.

  • Line 7: Prints a message indicating that the getter method is called.

  • Line 8: Returns the value of _radius.

  • Line 11: Defines a setter method set for setting the value of _radius.

  • Line 12: Checks if the given value is less than 0.

  • Line 13: Raises a ValueError if the given value is negative.

  • Line 14: Prints a message indicating the new value being set.

  • Line 15: Sets the value of _radius to the given value.

  • Line 18: Defines a deleter method delete for deleting the _radius attribute.

  • Line 19: Prints a message indicating that the _radius attribute is being deleted.

  • Line 20: Deletes the _radius attribute.

  • Line 23: Defines a property radius using the property() function, linking it to the getter, setter, and deleter methods.

  • Line 26: Creates an instance of the Circle class with an initial radius of 5.

  • Line 29: Prints the current value of the radius property.

  • Line 32: Sets a new value for the radius property.

  • Line 35: Deletes the radius attribute using the property.

Using class methods

In this method we create a class and override the descriptor methods __set____ get__, and __delete__ we need to use. This is especially useful when we need to use the same descriptor for multiple classes or attributes.

class CustomDescriptor:
def __init__(self, attribute_name=''):
self.attribute_name = attribute_name
def __get__(self, instance, owner):
return "{} for {}".format(self.attribute_name, owner.__name__)
def __set__(self, instance, value):
if isinstance(value, str):
self.attribute_name = value
else:
raise TypeError("Attribute name should be a string")
class MyClass:
attribute = CustomDescriptor()
# Create an instance of MyClass
my_instance = MyClass()
# Access the descriptor again
print(my_instance.attribute)
# Set the descriptor
my_instance.attribute = "ExampleAttribute"
# Access the descriptor again
print(my_instance.attribute)

In this example, the CustomDescriptor class is defined with an __init__ method initializing an attribute name, a __get__ method returning a formatted string containing the attribute name and the owning class name when accessed, and a __set__ method updating the attribute name if the assigned value is a string. The MyClass class utilizes an instance of CustomDescriptor as the attribute property. An instance of MyClass is created, and the attribute property is set to "ExampleAttribute". Let's look at the explanation of this code:

  • Line 1: Defines a new class named CustomDescriptor.

  • Line 2: Defines the initializer method (__init__) for the CustomDescriptor class, which takes an optional attribute_name argument.

  • Line 3: Initializes the instance variable attribute_name with the value passed to the constructor.

  • Line 5: Defines the __get__ method for the descriptor, which is called to get the attribute's value.

  • Line 6: Returns a formatted string combining the attribute_name and the owner's class name.

  • Line 8: Defines the __set__ method for the descriptor, which is called to set the attribute's value.

  • Line 9: Checks if the given value is a string.

  • Line 10: Sets the value of attribute_name to the given string value.

  • Line 11: Defines the alternative action if the value is not a string.

  • Line 12: Raises a TypeError if the given value is not a string.

  • Line 14: Defines a new class named MyClass.

  • Line 15: Creates an instance of CustomDescriptor and assigns it to the attribute class attribute.

  • Line 18: Creates an instance of the MyClass class.

  • Line 21: Prints the value of the attribute descriptor for the my_instance object.

  • Line 24: Sets the value of the attribute descriptor to "ExampleAttribute".

  • Line 27: Prints the value of the attribute descriptor for the my_instance object after setting it.

Using @property

The @property decorator in Python is used to create read-only properties for class attributes. By decorating a method with @property, the method is transformed into a property, allowing it to be accessed like an attribute rather than a method. This is particularly useful for encapsulating the logic of attribute access, enabling computed or dynamic attribute values without the need for explicit method calls.

class Person:
def __init__(self, name):
self._name = name
# Retrieve the person's name
@property
def name(self):
print('Get name')
return self._name
# Set the person's name
@name.setter
def name(self, new_name):
print('Set name to ' + new_name)
self._name = new_name
# Delete the person's name
@name.deleter
def name(self):
print('Delete name')
del self._name
# Create a person instance
p = Person('John Doe')
# Access and print the person's name
print(p.name)
# Update the person's name
p.name = 'Jane Doe'
# Delete the person's name
del p.name

In this example, the Person class has a name property implemented using the @property, @name.setter, and @name.deleter decorators. The property allows getting, setting, and deleting the person's name, demonstrating the use of decorators in a more personalized context. Now, let's look at the line by line explanation of this code:

  • Line 1: Defines a new class named Person.

  • Line 2: Defines the initializer method (__init__) for the Person class, which takes name as an argument.

  • Line 3: Initializes the instance variable _name with the value passed to the constructor.

  • Line 6: Decorates the method below as a property getter for name.

  • Line 7: Defines the getter method name for retrieving the value of _name.

  • Line 8: Prints a message indicating that the getter method is called.

  • Line 9: Returns the value of _name.

  • Line 12: Decorates the method below as a property setter for name.

  • Line 13: Defines the setter method name for setting the value of _name.

  • Line 14: Prints a message indicating the new value being set.

  • Line 15: Sets the value of _name to the given new_name.

  • Line 18: Decorates the method below as a property deleter for name.

  • Line 19: Defines the deleter method name for deleting the _name attribute.

  • Line 20: Prints a message indicating that the _name attribute is being deleted.

  • Line 21: Deletes the _name attribute.

  • Line 24: Creates an instance of the Person class with the name 'John Doe'.

  • Line 27: Accesses and prints the value of the name property, triggering the getter method.

  • Line 30: Sets a new value for the name property, triggering the setter method.

  • Line 33: Deletes the name attribute using the property, triggering the deleter method.

Conclusion

The purpose of using descriptors in Python is to customize attribute access in classes. Descriptors enable developers to define how attribute retrieval, assignment, and deletion should behave, allowing for controlled and dynamic behavior. This customization is particularly valuable when enforcing constraints, implementing computed attributes, or executing additional logic during attribute operations.

Moreover, descriptors represent a powerful and general-purpose protocol in Python, serving as the mechanism behind properties, methods, static methods, class methods, and super(). They are extensively utilized throughout Python itself, particularly for implementing the new-style classes introduced in version 2.2. Descriptors streamline the underlying C code and provide a versatile set of tools for everyday Python programming tasks.

Free Resources

Copyright ©2025 Educative, Inc. All rights reserved