Tomislav's blog

On Python's @property decorator

@property decorator is an excellent way to reduce the readability of Python code. It obfuscates a perfectly good function call and tricks readers into thinking they're performing a regular attribute access or assignment.

Unless there's a really good and explicit reason to do this, don't.

List of good and explicit reasons:

  1. Refactoring

That's pretty much it.

If you need to turn something that (rightfully so) started out as a simple attribute, but with time accrued some more complex logic, @property is a good way to gracefully transition from attributes to function calls.

Version 1

We start out with a simple attribute. You can get it, you can set it. As a consenting adult, you're free to do with it whatever you want.

class Client:
    def __init__(self, value):
      self.value = value

Version 2:

The project gains traction. You need to add two new features:

  1. Emit an event whenever the Client.value attribute is accessed, so other parts of the code can listen to it and do their own thing
  2. You want a central place to validate values being assigned, to avoid littering the rest of your codebase with error handling

Because we're a self-aware smol brain developer, we like plain old functions. We craft a plan to change the class interface to use getter/setter functions instead of direct attribute access. But since we're also responsible and respectful to our colleagues/clients, we don't just change the API abruptly. No, we will be emitting a deprecation warning for some time, and only introduce breaking changes in the API after we've given everyone ample time to migrate.

import warnings


class Client:
    def __init__(self, value):
        # We add a private attribute to hold the value
        self._value = None
        self.set_value(value)

    @property
    def value(self):
        # We can now emit a deprecation warning on 
        # each access, urging our users to migrate to the new API
        warnings.warn("A.value is deprecated, use A.get_value() instead!", DeprecationWarning)

        # ... and offload the act of retrieving the value 
        # to the newly-introduced function
        value = self.get_value()
        return value

    @property.setter
    def value(self, new_value):
        warnings.warn("A.value is deprecated, use A.set_value() instead!", DeprecationWarning)
        self.set_value(new_value)

    # We add getter/setter functions with the new logic
    def get_value(self):
        self._emit_event('value_access')
        return self._value

    def set_value(self, new_value):
        self._validate_value(new_value)
        self._value = new_value

Version 3:

Time has passed, and people have migrated to the new API. We're ready to make our lives easier, and simplify the codebase by removing the dirty @property. Life is good again.

class Client:
    def __init__(self, value):
        self._value = None
        self.set_value(value)

    def get_value(self):
        self._emit_event('value_access')
        return self._value

    def set_value(self, new_value):
        self._validate_value(new_value)
        self._value = new_value

Going a bit deeper

@property is an example of a descriptor. Descriptors are a neat Python construct that "lets objects customize attribute lookup, storage, and deletion". Some of the nicer things in life I enjoy are made using descriptors, namely Django's ORM.

But just because you can doesn't mean you should. We always strive for the least complex option, and if you're certain descriptors will make everyone's (not just yours!) lives easier, then go for it. Most of the time, though, plain functions are the way to go.

Stop worrying and learn to love the function call.