@property and Descriptors

Difficulty: Intermediate

Question

What is the @property decorator in Python? How does it relate to the descriptor protocol?

Answer

@property is a built-in decorator that turns a method into a managed attribute, allowing getter, setter, and deleter logic with clean dot-notation access.

Benefits: - Start with a simple attribute, add validation/computation later without changing the public API - Computed properties (derived from other attributes) - Validation on assignment

Descriptor Protocol: any object that implements __get__, __set__, or __delete__ is a descriptor. @property is a built-in descriptor. You can create custom descriptors for reusable attribute behaviors.

Code examples

@property with getter, setter, deleter

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius  # Private backing attribute

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError('Temperature below absolute zero!')
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        del self._celsius

    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

    # Read-only computed property
    @property
    def kelvin(self):
        return self._celsius + 273.15

t = Temperature(25)
print(t.celsius)     # 25
print(t.fahrenheit)  # 77.0
print(t.kelvin)      # 298.15

t.celsius = 100
print(t.fahrenheit)  # 212.0

try:
    t.celsius = -500
except ValueError as e:
    print(e)

The @celsius.setter validates input before storing. fahrenheit and kelvin are computed properties - always derived from _celsius.

Custom Descriptor

class Validated:
    """Reusable descriptor for validated numeric attributes."""

    def __init__(self, min_val, max_val):
        self.min_val = min_val
        self.max_val = max_val
        self.name = None  # Set by __set_name__

    def __set_name__(self, owner, name):
        self.name = name  # Called when class is created

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self  # Accessed from class, not instance
        return getattr(obj, f'_{self.name}', None)

    def __set__(self, obj, value):
        if not (self.min_val <= value <= self.max_val):
            raise ValueError(
                f'{self.name} must be between {self.min_val} and {self.max_val}'
            )
        setattr(obj, f'_{self.name}', value)

class Employee:
    age = Validated(18, 65)
    salary = Validated(0, 1_000_000)

    def __init__(self, name, age, salary):
        self.name = name
        self.age = age       # Calls Validated.__set__
        self.salary = salary # Calls Validated.__set__

e = Employee('Alice', 30, 75000)
print(e.age, e.salary)

try:
    e.age = 15  # ValueError
except ValueError as e:
    print(e)

__set_name__ is called when the class is defined, passing the attribute name. Descriptors enable reusable validation across multiple class attributes.

Key points

Concepts covered

@property, getter, setter, deleter, Descriptor Protocol