Beyond @property - Unleashing the Power of Python Descriptors

Published on 17 June 2025
5 min read
Python
Beyond @property - Unleashing the Power of Python Descriptors

There are many ways to deal with object access out there, you could follow C++, C# for example that normally uses getters and setters, or even in a more pythonic way using @property + @property.setters decorators but today we will work with Python descriptors.

What is Python Descriptors?

Descriptors works like a wrapper around a property, in the high level this will act like getters and setters, wrapping the access of a property, allowing access control, data validation, logging or anything you want to do as you would in a normal python function. Ok, this until now doesnt seems to be too nice to use if this at the end is basically a function, isnt? Actually the possibilities with descriptors are I would say infinity but let’s dive into an example to get a better view of it.

Before continue, please take a look at this requirements:

  • Previous Python Knowledge
  • Python 3.10+

Lets define the Descriptor:

sample.py
python
class DescriptorClass:

    def __init__(self, attr_validator = None, attr_access_control = None):
        self.attr_validator = attr_validator
        self.attr_access_control = attr_access_control
        self.attr_name = None

    def __set_name__(self, owner, name):
        # This function is called when the instance of the descriptor is created in your class
        # when this happens python will call it so we can map the name of the attribute we have to deal
        # or the owner of it if you want, that is the class you have used the descriptor
        self.attr_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        # obj here is the instance that have the descriptor
        # objtype is the class type of the obj
        if self.attr_access_control and self.attr_access_control(obj) is False:
            print("not allowed to access value")
            return

        if obj is None:
            # return self here allows us to dynamically access the meta values from descriport comming from the class that is using it
            # E.g YourClass.your_descriptor_attr.attr_validator | ...attr_access_control and etc...
            # in other words: allow metadata access when accessed from the class rather than an instance
            return self

        return getattr(obj, self.attr_name)
    
    def __set__(self, obj, value):
        # obj here is the same as in __get__
        # value is the value you are trying to set
        if self.attr_access_control and self.attr_access_control(obj) is False:
            print("not allowed to change value")
            return
        
        if self.attr_validator:
            self.attr_validator(value)
        setattr(obj, self.attr_name, value)

Now, we will need a few dummy validators to use in our example

sample.py
python
def number_validator(value):
    if not isinstance(value, int):
        raise ValueError("Value must be an integer")
    if value < 0:
        raise ValueError("Value must be a positive integer")

def string_validator(value):
    if not isinstance(value, str):
        raise ValueError("Value must be an string")

    if len(value) < 2 or len(value) > 10:
        raise ValueError("Value must lenght between 2 and 10")

def refuse_access_if_admin_flag_is_not_set(obj):
    if hasattr(obj, "admin_flag"):
        return getattr(obj, "admin_flag") == True
    
    return False

And last, a class to play around

sample.py
python
class YourClass:

    my_number = DescriptorClass(number_validator)
    my_string = DescriptorClass(string_validator, refuse_access_if_admin_flag_is_not_set)

    def __init__(self, num, string):
        self.my_number = num
        self.admin_flag = True
        self.my_string = string

With all this sample code in place I think i give a better idea of the power of Descriptors, its easy to visualize that the extension of this validadors to make something else is completly possible. E.g you could log a specific string pattern when some value is changed, or even are close to some boundaries and you want to track this using some alert system or whatever you have in place to watch the values you want…

Playing around:

terminal
python
>>> from main import YourClass
>>> obj = YourClass(2, "nice value")
>>> obj.my_number
2
>>> obj.my_string
'nice value'
>>> obj.admin_flag
True
>>> obj.admin_flag =  False
>>> obj.my_string
not allowed to access value
>>> obj.my_string = "random value"
not allowed to change value
>>> obj.admin_flag = True
>>> obj.my_string = "cc"
>>> obj.my_string = "c"
Traceback (most recent call last):
  ...
ValueError: Value must lenght between 2 and 10
>>> obj.my_string = "ccccc"
>>> obj.my_string = "cccccccccccccc"
Traceback (most recent call last):
  ...
ValueError: Value must lenght between 2 and 10
>>> obj.my_number = "hi"
Traceback (most recent call last):
  ...
ValueError: Value must be an integer
>>> obj.my_number = -1
Traceback (most recent call last):
  ...
ValueError: Value must be a positive integer
>>> obj.my_number = 5
>>> obj.my_number
5
>>> obj.my_string
'ccccc'

Conclusion

Descriptors is a powerful tool as you all can see, it could allow you to extend your code functionality while keeping it safe to use and easy to understand. It’s important to note that while I said before when starting to explain what Descriptors are, comparing with property decorator and etc…, !!! in fact I would love to enforce here that Descriptors are not an alternative to @property - it’s way more powerful than that and also just so you know, @property is built using Descriptors as are @classmethod and @staticmethod.

Thank you for reading! I hope you learned a little bit. If you have any complaint or want to talk please, drop me a message on linkedin!