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
- When accessed calls the
- 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