Skip to content

The `property` Decorator for Creating Computed Properties in Python

Updated: at 03:01 AM

The property decorator in Python allows you to define computed attributes in classes that act like regular attributes. This enables cleaner, more Pythonic code by avoiding explicit getter and setter methods.

In this comprehensive guide, we will cover the following topics:

Table of Contents

Open Table of Contents

What Are Computed Properties?

In object-oriented programming, a computed property (also known as derived property) is a property whose value is computed based on other data members rather than stored.

For example, let’s say we have a Person class with first_name and last_name data attributes. We could compute a full_name property by concatenating the first and last names.

Computed properties allow abstraction and encapsulation by hiding complexity behind simple, readable attributes. Developers using the class don’t need to know how the property is calculated under the hood.

The Need for Computed Properties

Here are some common cases where computed properties are useful in Python:

Without computed properties, we would need to implement these behaviors using getter and setter methods. But this can bloat the class with many single-purpose methods. The property decorator offers a cleaner and more Pythonic approach.

Introducing the property Decorator

The built-in property decorator allows us to define computed properties in Python classes. Here is its signature:

property(fget=None, fset=None, fdel=None, doc=None)

The property constructor takes up to 4 arguments, all optional:

The property decorator handles turning these helper functions into computed attribute accesses on your class instances.

We will go over usage of these arguments in more detail in the following sections.

How to Use the property Decorator

The property decorator is placed directly above the method that you want to make a computed property. This method becomes the getter function.

Here is a simple example:

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

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

p = Person("John", "Doe")
print(p.full_name) # Calls the full_name() method
# Prints "John Doe"

We decorate the full_name() method with @property. This signals that full_name should be accessed as an attribute instead of a method call.

Behind the scenes, Python runs the full_name method and returns the value when we access p.full_name. No need for an explicit get_full_name() call!

Now let’s go over the 4 decorator arguments more in-depth, starting with the getter function.

The Getter Function

The getter function computes and returns the property’s value. This is required for a basic read-only property.

Here are some key notes on writing getter functions:

Let’s add a getter for a full_name_upper property to our earlier example:

class Person:
    # ...

    @property
    def full_name_upper(self):
        """Returns the full name in uppercase"""
        if not self.full_name:
            raise AttributeError("No full name assigned yet")

        return self.full_name.upper()

Now we have a read-only computed property that raises an error if full_name hasn’t been defined yet.

Property Setters and Deleters

To make a modifiable property, we can define a setter and/or a deleter function.

Setter Function

The setter allows assigning values to the property through attribute access.

@full_name.setter
def full_name(self, value):
    self.first_name, self.last_name = value.split(' ', 1)

The setter takes self and the new value as arguments. It updates the underlying data members that the property relies on.

Use the setter for:

Deleter Function

The deleter allows deleting the property using del.

@full_name.deleter
def full_name(self):
    self.first_name = None
    self.last_name = None

This clears the first and last name attributes if del p.full_name is called.

The deleter doesn’t take any arguments besides self. Use it to reset dependent data members when the property is deleted.

Property Documentation

The doc argument can be used to provide a docstring for your property.

@property
def full_name(self):
    """Read-only full name property"""
    ...

The docstring should clearly explain how the property functions and any side effects.

You can also set the docstring on the getter, setter or deleter methods instead.

Good documentation helps other developers use your computed properties correctly.

When to Use the property Decorator

Now that we’ve covered how to use property, let’s discuss when it’s most appropriate.

For “Computed” Properties

The property decorator shines when creating properties whose values are computed in some way.

For example, these are all good candidates for property:

Basically, use property for any attribute that relies on a calculation or transformation of some kind.

For Encapsulation and Abstraction

The property decorator allows encapsulation of internal state. Other code can read and modify properties without accessing private internal data directly.

It also enables abstraction by hiding implementation details behind a simple interface.

Think of properties as the public API for your class. Keep the internals private.

For Read-Only Attributes

The simplest way to create a read-only property is by adding @property above a getter method without a setter.

Read-only properties give us more control over attributes and prevent accidental modification.

For example, identifiers, constants, and aggregated values often don’t need modification after initialization.

When You Need More Than Field Access

Sometimes assigning to an attribute should do more than just store the value.

For example, you may want to:

The property setter lets you handle these scenarios without giving up clean attribute access.

Advantages and Disadvantages

Let’s discuss some notable pros and cons of using the property decorator:

Advantages:

Disadvantages:

In most cases, the increased encapsulation and cleaner APIs outweigh the downsides. But consider both perspectives when deciding between properties and regular methods.

Real World Examples

To better illustrate practical use cases for the property decorator, let’s go through some real-world examples:

1. Validating Data

We can validate data before setting it using the property setter. For example:

class Person:

    def __init__(self, age):
        self.age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

Now creating a Person with a negative age will validate and raise an error rather than storing an invalid value.

2. Modifying Dependent Properties

Let’s model a car with integer fields for year, make, model and a formatted name property:

class Car:

    def __init__(self, year, make, model):
        self.year = year
        self.make = make
        self.model = model

    @property
    def name(self):
       return f"{self.year} {self.make} {self.model}"

    @name.setter
    def name(self, value):
        # Extract year, make, model from value
        # Set respective fields

When a user sets car.name = "2012 Tesla Model S", the property setter decomposes this into the underlying fields.

3. Caching Expensive Lookups

We can use a property to cache data after performing an expensive computation or lookup:

class Image:

    def __init__(self, path):
        self.path = path
        self._size = None

    @property
    def size(self):
        if self._size is None:
            # expensive size lookup
            self._size = determine_image_size(self.path)

        return self._size

The size property getter performs the one-time lookup on first access and caches it. Subsequent accesses return the cached value instantly!

Common Mistakes

While the property decorator is quite powerful, it’s easy to misuse it if you aren’t familiar with some of the pitfalls:

Forgetting to Call the Property

Don’t forget that a property must be called like a method even without arguments.

This is invalid:

p = Person()
print(p.full_name) # Broken! Forgot ()

You must call it like a method by adding () even if it takes no arguments:

print(p.full_name()) # Works!

Using a Property Like a Class Field

Properties do not behave exactly like regular class attributes. Avoid setting them in __init__ or class definitions:

# Broken! Sets attribute on class not instance
class Person:
    full_name = "John Doe"

# Also broken!
p = Person()
p.full_name = "Jane Doe"

Treat properties like methods and call them to get/set values.

Forgetting to Return a Value

It’s a common pitfall for new Python programmers to forget that properties need to return a value:

@property
def full_name(self):
    print(f"{self.first} {self.last}") # Oops!

This will run without error, but return None instead of the name. Remember to use return.

Causing Side Effects

Avoid side effects and mutable state in property getters and setters. Users may not expect repeated calls to the same property to produce different results.

Combining @property and @classmethod

The @property decorator works on instance methods. For class-level properties, use @classmethod instead:

@classmethod
@property
def max_users(cls):
  return cls.max_active_users

Conclusion

The property decorator is an elegant way to implement computed attributes in Python. It improves encapsulation and reduces boilerplate code compared to manual getter and setter methods.

Key takeaways:

While the property decorator takes some experience to master, it’s a versatile tool that all Python programmers should know. Use it to write more Pythonic classes that provide a clean yet powerful interface.