Python @property and Inheritance

Python @property and Inheritance

Encapsulation helps create a black box from an object that hides the internal state and requires all interaction to be performed through an object’s methods. Property getters and setters are tools that Python provides to help us implement encapsulation in our code. Getters can control how the data is accessed and setters can include checks and validations to ensure that the attribute is set to a safe and valid value. In this way, the details of how an attribute is stored or calculated are not exposed to the outside world.

Encapsulation also helps to separate the interface from the implementation of the objects. By using property decorators, we can create a simple and consistent interface, so users won’t need to know whether they are accessing a simple attribute or a method that computes a value. Even if the data itself is stored differently over time, the way that it is accessed doesn’t need to change. Late we can change how attributes are computed or stored without affecting the class interface. It’s also a way to implement abstraction by providing a simplified interface to complex operations behind the scenes.

In Python, one can use @property like this:

class Number:
    """
    >>> n = Number(-4.1)
    >>> print(n.value)
    -4.1
    """
    def __init__(self, value):
        """Uses property setter."""
        self.value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value = new_value


class Integer(Number):
    """
    >>> i = Integer(-4.1)
    >>> print(i.value)
    -4
    """
    @property
    def value(self):
        return super().value

    @value.setter
    def value(self, new_value):
        _value = int(new_value)
        super(Integer, type(self)).value.fset(self, _value)

When does @property not work?

I want you to introduce one inheritance case that @property doesn’t work.

Assume we have two mixins, we are calling a super method from our class instance. Mixin2 doesn’t have the callee method but Mixin1 does have it. So the call will fall into the method residing inside Mixin1.

class PaymentPage(Mixin2, Mixin1, OrderPage):
    input_serializers = {
        PaymentGateway.stripepay: serializers.StripePaymentSerializer,
        PaymentGateway.yandexpay: serializers.YandexPaymentSerializer,
        PaymentGateway.googlepay: serializers.GooglePaymentSerializer,
    }

    @property
    def input_serializer(self):
        pos = self.order.get_pos()
        return self.input_serializers.get(pos.gateway)

    def validate(self, **kwargs):
        self.validate_inputs(self.order, **kwargs)

class Mixin2(object):
    # doesn't have validate_inputs method
    ...

class Mixin1(object):
    # the method call will hit here
    def validate_inputs(self, order=None, **kwargs):
        serializer = self.input_serializer(order=order, **kwargs)
        serializer.is_valid()

When you run the validate() method, Mixin1.validate_inputs method will be run. But at this point, the program will fail because @property is NOT passed as an actual property to classes in the method resolution, in our case to mixins. “TypeError: ‘NoneType’ object is not callable”. The @property is available only inside the class itself or inside the child classes like a protected property. So in this case you need to initialize your property as a class attribute input_serializer=...Serializer and then use it somewhere else.

Resources:
Python @property inheritance the right way