How to use the @property decorator in Python and Django

January 31st, 2021

·

5 min read

Photo by Sarah Dorweiler

In Python, the @property decorator allows you to call custom model methods as if they were normal model attributes.

For example, if you have the following greeting method,

class Person:
    def __init__(self, first_name):
        self.first_name = first_name

    def greeting(self):
        return f'Hello {self.first_name}!'
>>> person = Person(first_name='Andrea')
>>> person.greeting()
Hello Andrea!

adding the @property decorator to that method would allow you to access its computed value like a model attribute without parenthesis:

[...]

@property
def greeting(self):
    return f'Hello {self.first_name}!'
>>> person.greeting
Hello Andrea!

What is a decorator?

In Python, the function is a first-order object. It means that it can be passed as an argument to another function. It is also possible to define a function inside another function. Such a function is called a nested function. Moreover, a function can return another function.

A decorator is a function that receives another function as argument. The behaviour of the argument function is extended by the decorator without actually modifying it.

In the example below, you can see what a typical decorator function looks like in Python:

def decorator(f):
    def new_function():
        print("Extra Functionality")
        f()

    return new_function

@decorator
def initial_function():
    print("Initial Functionality")
>>> initial_function()
Extra Functionality
Initial Functionality

The f is a function whose behaviour we want to extend. So, we will have to write a custom function like decorator(), which takes the f as an argument.

The new_function() is an inner function where we can write additional code to extend the behaviour of the f, before or after calling it. 

And finally, the new_function() should be returned. In this way, Python includes decorator functions. Also, we can define our own decorator function to extend the behaviour of a function without modifying it.

How does the @property decorator work?

The @property decorator is a built-in decorator in Python for the property() function. This function returns a special descriptor object which allows direct access to getter, setter, and deleter methods.

A typical use is to define a managed attribute x:

class C(object):
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x

    def setx(self, value):
        self._x = value

    def delx(self):
        del self._x

    x = property(getx, setx, delx, "I'm the 'x' property.")

If c is an instance of C, c.x will invoke the getter, c.x = value will invoke the setter and del c.x the deleter. This makes it possible to create read-only properties easily using property() as a decorator.

The following example explains how to use the @property decorator to achieve the same behaviour as the first example:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

In this last example, the property object has getter, setter, and deleter methods usable as decorators that create a copy of the property with the corresponding accessor function set to the decorated function.

You don't necessarily have to define all three methods for every property. You can define read-only properties by only including a getter method. You can choose which methods to include depending on the context that you are working with.

Why use it?

Computed attributes

In Django, a typical situation where the @property decorator can be used is for model methods that return computed attributes.

For example, if you have a Book model with a price attribute, it can be useful to create a custom method to calculate the price_locale string:

from django.db import models
import locale

class Book(models.Model):
    price = models.IntegerField()
    
    @property
    def price_locale(self):
        locale.setlocale(locale.LC_ALL, '')
        return locale.currency(self.price)
>>> book = Book(price=30)
>>> book.save()
>>> book.price_locale
'£30.00'

Backward compatibility

Another scenario when the @property decorator comes to our rescue is when we need to convert a model attribute from public to private (i.e. from price to _price). By adding a getter method for the _price attribute, we can keep all the code where we access that attribute as it is, making use of the read-only accessor.

Existing code:

class Book(models.Model):
    price = models.IntegerField()

Code after refactoring:

class Book(models.Model):
    _price = models.IntegerField()

    @property
    def price(self):
        return self._price

Django limitations

Although using the @property decorator can help you access model methods as attributes, you can't filter QuerySets by them, use them for ordering, or any other operation that makes use of the database engine.

So, before deciding whether creating a new model attribute or just adding a custom method with the @property decorator, think about the type of operations you would like to execute using that field.

Further reading

A big thank you to the following resources, which greatly helped me grasp the concepts described in this article: