Python has many features, and it's easy to get overwhelmed while learning. Today we will focus on learning just one of those features: the @property decorator.

We will explain what properties are, why you might want to use them, and where they fit within Python's class structure.

When learning to program there is often a steep learning curve after you learn the basics. There is a big gap between tutorials and real-world projects! The @property decorator is one of those features that you seldom see in tutorials but that can be very useful at times.

What are properties? What are methods?

Too often terms like property and method are thrown around when discussing programming languages. It's useful to slow down to define what each term actually means in Python.

An attribute is a variable that is specific to a class object. If you are unfamiliar with what an "object" is, check out this article for a quick review. For example, if you have a class called Human, an attribute would be any variable that exists on each object of that class. You create attributes by using the self keyword. "Attribute" is just the name we give to variables that belong to a class. Here's an example, using the Human class:

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

self.age refers to the attribute that is now on the objects of the Human class. We can set up many attributes, depending on what we need the class objects to have. For example, we could set self.hair_color or self.eye_color as attributes of objects of the Human class.

A method is a function that belongs to a class. For example, in the class called Human, a method would be any function that exists inside of that class. Here's an example, using our Human class:

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

    def get_age(self):
        return self.age

The get_age function is called "a method on the class Human". Important: Methods always receive the argument self as their first parameter. This is because methods are always connected to a class. In this case, get_age is what's called an "instance method", and will always receive the object that it is being called on as the first argument. To learn more about each type of method, check out this article.

A property is a special type of attribute. It is an attribute that has two methods assigned to it: a "getter", and a "setter". Let's look at some code to explain further:

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

    def get_age(self):
        return self.age

    def set_age(self, new_age):
        self.age = new_age

    age = property(get_age, set_age)

As you can see, we are using much of the same code. We included a new "setter" function called set_age, and then we identified the age variable as a property of the class, with the getter of get_age and the setter of set_age. That is what the property() function is doing: it takes a getter for the first parameter, a setter for the second parameter, and creates a property variable on the class, which it stores in the name that you assign.

Properties on classes are useful because they allow us to do some dynamic processing on the attributes when we get them or set them. For example, if your class had the attribute age, but you wanted to make sure that the age value was always given in months, you could ensure that by making the age attribute a property. Check out this code to see what I mean:

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

    def get_age_in_months(self):
        return self.age * 12

    def set_age(self, new_age):
        self.age = new_age

    age = property(get_age_in_months, set_age)

Decorators

To further streamline the process of creating properties, we will use the @property decorator. If you haven't ever used/seen decorators, follow our mini-course to get up to speed quickly.

The @property decorator works as a shortcut to using the property() function that we used earlier. On top of that, it makes the code a little bit cleaner. In addition, the @property decorator executes first upon class initialization, so there's no chance to accidentally use the attribute before it becomes a property. Let's dive in by checking out this code:

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

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

    @property 
    def months(self):
        return self._age * 12

    @age.setter
    def age(self, new_age):
        self._age = new_age

Here we use decorators on our getter and setter functions, which we have now renamed to be the name of the attribute. Let's go through each one specifically.

The getter method, age, is what gets the @property decorator applied to it. We can use this to format and change what the getter outputs every time the property is referenced.

But since age is a function, we still need to store the value of the human's age somewhere. That's where the _age attribute comes into play.

When an object of class Human is initialized, the __init__ method will run. This sets the age attribute on the self object. Because we wrote a custom setter for the age property, the custom setter will run.

In the custom setter, we take the incoming age value and set the _age attribute on the self object. At this point, using the setter, we could have more control over how the _age attribute is set.

When the __init__ method finishes running, the object will have:

  • An _age attribute which contains the value passed in when the object was created.
  • An age property which:
    • When accessed calls the @property method
    • When changed uses the @age.setter method
  • A months property that can be accessed but whose value cannot be changed (since there's no setter for it).

To show what I mean, consider the following code:

jeff = Human(26)

print('I am ')
print(jeff.age)
print(' years old.')

What do you think this will print?

This will output the following:

I am 
26
years old.

Decorating the getter function with the @property decorator allows us to change how the property will be served to anyone that calls on it. For example, I can use the code to return the amount of months that the age equates to by creating another property,months:

@property
def months(self):
        return self._age * 12

Now, anytime that the variable jeff.months is called, it will return:

312

Let's dive into the setter. Now that we have defined a property using the @property decorator, we can write a setter for it using @[property_name].setter. Remember to replace property_name with the actual property name inside your class.Let's look at how we did this in the Human class:

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

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

    @age.setter
    def age(self, new_age):
        self._age = new_age

The setter on the age property is decorated with the @age.setter decorator, which allows us to modify how this property is allowed to be set. For example, we could restrict someone's age to be only whole numbers by doing this:

@age.setter
def age(self, new_age):
    if not isinstance(new_age, int):
    raise TypeError("Age must be an integer.")
    self._age = new_age

Now, if we try to run the following code:

print(jeff.age)

jeff.age = 44.5

print(jeff.age)

jeff.age = 50

print(jeff.age)

Here's what the output will be:

26
Traceback (most recent call last):
  File "main.py", line 3, in <module>
TypeError: Age must be an integer.

So you can see that our custom setter function will prevent anything but integers from being set onto the age property. Because we raised a TypeError, the program will stop running whenever we try to set a non-integer value on the age property. The error message is shown together with a traceback that tells us where the error occurred.

Conclusion

There's the @property decorator in a nutshell! In all, it is a very powerful way to gain more control over how the attributes are set and formatted inside of your classes. By using custom getters, you can modify how you present the attributes. By using custom setters, you can control how each property is allowed to be set.

If you want to learn more about Python, consider enrolling in our Complete Python Course which takes you from beginner all the way to advanced (including OOP, web development, async development, and much more!). We have a 30-day money-back guarantee, so you really have nothing to lose by giving it a try. We'd love to have you!

Photo by Safar Safarov on Unsplash