DEV Community

Cover image for A Practical Guide to Python's @property Decorator (with Examples)
Amoh Gyebi Ampofo
Amoh Gyebi Ampofo

Posted on

A Practical Guide to Python's @property Decorator (with Examples)

This tutorial is for intermediate Python learners who are already familiar with simple Python syntax, such as if...else statements.

What is @property?

The Python @property decorator is used to turn a method into a property. This means you can interact with the method as if it were an attribute. For instance, you can assign a value to it with person.salary = 4500 or access its value without using parentheses, like print(person.salary). This is all possible if the method has been decorated with @property.

Why Would You Want to Use @property?

The @property decorator is particularly useful when you have an attribute that requires validation or sanitization to prevent corruption. For example, imagine you have a percentage value that needs to be displayed on a user interface. To fit your UI design, you might want to ensure it always has a maximum of two decimal places. You could add extra code to trim the value accordingly:

percent = 3/2346 * 100  # 0.1278772378516624
percent = float(f'{percent:.2f}') # Corrected to use f-string for formatting
Enter fullscreen mode Exit fullscreen mode

You could encapsulate this logic in a function or method:

def calc_percent(old) -> float:
    return float(f'{old:.2f}') # Corrected to use f-string for formatting
Enter fullscreen mode Exit fullscreen mode

But what if you could set a new value to it and access it just like a variable?

calc_percent = 15
print(calc_percent)
Enter fullscreen mode Exit fullscreen mode

This can be achieved by using the @property decorator on a class method. You will need two methods: one that Python uses to set the value (the setter) and another to get the value (the getter). We will discuss this in the "How" section.

When Should You Use @property?

There are various instances where using a @property is beneficial. Some common use cases include when you want to:

  • Make a property read-only.
  • Prevent the deletion of a property.
  • Ensure all elements in a collection are homogeneous.
  • Prevent your code from breaking because a property doesn't exist or a list is empty.

How to Use @property

To use the @property decorator, you must work with methods inside a class.

Turning a Method into a Property

With the @property decorator, you can make a method behave like an attribute.

class ProgressReport:
    def __init__(self) -> None:
        self._progress = 19

    @property
    def progress(self):
        return self._progress

reporter = ProgressReport()
print(reporter.progress)
Enter fullscreen mode Exit fullscreen mode

Output:

19
Enter fullscreen mode Exit fullscreen mode

In the code above, progress becomes a property. When you access reporter.progress, it calls the progress method and returns the value of self._progress. The _progress attribute is treated as a private property by convention. The leading underscore signals to other developers that it should not be set directly.

Printing the type of the property shows no trace that it is a method:

print(type(reporter.progress))
Enter fullscreen mode Exit fullscreen mode

Output:

<class 'int'>
Enter fullscreen mode Exit fullscreen mode

The property decorator also allows you to define a "setter" method, which is called whenever you assign a value to the property. This enables you to control how values are set.

class ProgressReport:
    def __init__(self) -> None:
        self._progress = 19

    @property
    def progress(self):
        return self._progress

    @progress.setter
    def progress(self, value):
        if 0 <= value <= 100: # More Pythonic range check
            self._progress = value
        else:
            raise ValueError("Progress must be a value between 0 and 100") # More specific exception

reporter = ProgressReport()
reporter.progress = 500
print(reporter.progress)
Enter fullscreen mode Exit fullscreen mode

Output:

Traceback (most recent call last):
  File "C:\Users\Ampofo\property.py", line 19, in <module>
    reporter.progress = 500
  File "C:\Users\Ampofo\property.py", line 16, in progress
    raise ValueError("Progress must be a value between 0 and 100")
ValueError: Progress must be a value between 0 and 100
Enter fullscreen mode Exit fullscreen mode

In this example, we ensure that the property can only be set if the new value is between 0 and 100.

Making a Property Read-Only

You can make a property read-only by defining a setter that always raises an exception, preventing its value from being changed directly. However, you can still modify the underlying private attribute within the class.

Here, we create a method, update_progress, that we use to update the value of the property internally.

class ProgressReport:
    def __init__(self) -> None:
        self._progress = 19

    @property
    def progress(self):
        return self._progress

    @progress.setter
    def progress(self, value):
        raise AttributeError("progress is read-only") # More appropriate exception

    def update_progress(self, value):
        # You might add validation here as well
        if 0 <= value <= 100:
            self._progress = value
        else:
            raise ValueError("Progress must be a value between 0 and 100")

reporter = ProgressReport()
reporter.update_progress(96)
print(reporter.progress)
reporter.progress = 99
Enter fullscreen mode Exit fullscreen mode

Output:

96
Traceback (most recent call last):
  File "C:\Users\ampofo\property.py", line 20, in <module>
    reporter.progress = 99
  File "C:\Users\ampofo\property.py", line 12, in progress
    raise AttributeError("progress is read-only")
AttributeError: progress is read-only
Enter fullscreen mode Exit fullscreen mode

If you do not intend to allow any modifications, you can omit the setter and any internal update methods entirely.

Controlling Deletion

You can also control the deletion of a property using @progress.deleter.

class ProgressReport:
    def __init__(self) -> None:
        self._progress = 19

    @property
    def progress(self):
        return self._progress

    @progress.setter
    def progress(self, value):
        raise AttributeError("progress is read-only")

    @progress.deleter
    def progress(self):
        print('Performing housekeeping before deletion...')
        del self._progress

reporter = ProgressReport()
del reporter.progress
print(reporter.progress)
Enter fullscreen mode Exit fullscreen mode

Output:

Performing housekeeping before deletion...
Traceback (most recent call last):
  File "C:\Users\ampofo\property.py", line 25, in <module>
    print(reporter.progress)
  File "C:\Users\ampofo\property.py", line 8, in progress
    return self._progress
AttributeError: 'ProgressReport' object has no attribute '_progress'
Enter fullscreen mode Exit fullscreen mode

As you can see, we performed some housekeeping tasks before deleting the property. If we wanted to prevent its deletion entirely, we could have simply not included the del self._progress line in the deleter method.

When Not to Use @property

In instances where you want to control the behavior of a container like a list or a dict, the @property decorator is not the right tool. But that shouldn't stop us! As developers, we find solutions to every problem. Python allows you to customize any type by subclassing it. We can subclass list and dict and add our modifications.

Customizing Lists

Normally, trying to access a non-existent index in a list results in an error:

list1 = [1, 2, 3]
print(list1[5])
Enter fullscreen mode Exit fullscreen mode

Output:

Traceback (most recent call last):
  File "C:\Users\ampofo\property.py", line 2, in <module>
    print(list1[5])
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

By subclassing list, you can create your own list class that changes this behavior. This way, you can prevent your code from breaking when an invalid index is accessed and instead return None.

class MyList(list):
    def __getitem__(self, index):
        try:
            return super().__getitem__(index)
        except IndexError:
            return None

list2 = MyList([1, 2, 3])
print(list2[5])
Enter fullscreen mode Exit fullscreen mode

Output:

None
Enter fullscreen mode Exit fullscreen mode

In the code above, we subclass list by creating a MyList class. We override the __getitem__ method, which is called whenever an item of the list is accessed (e.g., names[0]). Our new implementation returns None rather than raising an IndexError.

You can further customize other methods, like append, to enforce specific rules.

class MyHomogeneousList(list):
    def append(self, value):
        if not isinstance(value, int): # isinstance is the preferred way to check types
            print('This list only accepts integers.')
        else:
            super().append(value)

list2 = MyHomogeneousList([1, 2, 3])
list2.append('4')
list2.append(4)
print(list2)
Enter fullscreen mode Exit fullscreen mode

Output:

This list only accepts integers.
[1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Customizing Dicts

If you want to modify the behavior of a dict, your best option is also to subclass it and introduce your alterations.

class MyDict(dict):
    def __setitem__(self, key, value) -> None:
        print(f"Inserting {key}")
        return super().__setitem__(key, value)

    def __getitem__(self, key):
        if key not in self:
            print(f"Key: '{key}' does not exist.")
            return None
        return super().__getitem__(key)

    def __delitem__(self, key) -> None:
        if key == 'admin':
            print("You can't delete the 'admin' entry.")
            return
        return super().__delitem__(key)

dict1 = MyDict({'admin': 'active'})
print(dict1['johndoe'])
dict1['janeaustin'] = 'inactive'
print(dict1)
del dict1['admin']
print(dict1) # Added to show the entry was not deleted
Enter fullscreen mode Exit fullscreen mode

Output:

Key: johndoe doesn't exits
None
Inserting janeaustin
{'admin': 'active', 'janeaustin': 'inactive'}
You can't delete admin entry
Enter fullscreen mode Exit fullscreen mode

Read more here
Buy my book on How not to become a broke software engineer here

Cover Photo by Tai Bui on Unsplash
Thank you for reading.

Top comments (0)