Learn how to use properties to add dynamic behaviour to your attributes.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
Properties, defined via the property
built-in, are a Python feature that lets you add dynamic behaviour behind what is typically a static interface: an attribute.
Properties also have other benefits and use cases, and we will cover them here.
In this Pydon't, you will
property
;property
to implement read-only attributes;property
doesn't have to be used as a decorator;property
in the standard library.You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
A property is an attribute that is computed dynamically. That's it. And you will understand what this means in a jiffy!
This is a class Person
with three vanilla attributes:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
self.name = f"{self.first} {self.last}"
john = Person("John", "Doe")
However, there is an issue with the implementation. Can you see what is the issue with this implementation?
I'll give you a hint:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.last = "Smith"
>>> john.name
# ?
When you implement name
as a regular attribute, it can go out of sync when you change the attributes upon which name
depended on.
How do we fix this?
Well, we could provide methods that the user can use to set the first and last names of the Person
instance, and those methods could keep name
in sync:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
self.name = f"{self.first} {self.last}"
def set_first(self, first):
self.first = first
self.name = f"{self.first} {self.last}"
def set_last(self, last):
self.last = last
self.name = f"{self.first} {self.last}"
john = Person("John", "Doe")
This works:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.set_first("Charles")
>>> john.name
'Charles Doe'
However, we had to add two methods that look pretty much the same... And this would get worse if we introduced an attribute for middle names, for example... Essentially, this isn't a very Pythonic solution – it isn't very elegant. (Or, at least, we can do better!)
There is another alternative...
Instead of updating name
when the other attributes are changed, we could add a method that computes the name of the user on demand:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
def get_name(self):
return f"{self.first} {self.last}"
This also works:
>>> john = Person("John", "Doe")
>>> john.get_name()
'John Doe'
>>> john.first = "Charles"
>>> john.get_name()
'Charles Doe'
But this isn't very elegant, either. However, it serves as an excellent starting point for our discussion! How can we replace this “getter method” with a property?
The Pythonic solution to this problem of computing an attribute dynamically – because the name of a person is essentially just an attribute of a person – comes in the form of properties!
To define a property:
Then, you wrap that method in @property
.
So, we take the code for the method get_name
, we change the name of the method to match the name of the attribute (name
), and then add the decorator:
How do you use this?
Well, you just access the attribute name
as you regularly would.
Behind the scenes, Python will run the method name
and return the value:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.last = "Smith"
>>> john.name
'John Smith'
Congratulations! You just wrote your first property!
As we have seen before, properties are excellent for when you want to have an attribute that depends dynamically on something else.
For the example above, the attribute Person.name
depended on two other attributes:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
@property
def name(self):
return f"{self.first} {self.last}"
We can extend our class Person
to keep track of the birth date of the person.
Then, we can add an attribute age
that returns the age of the person:
import datetime as dt
class Person:
def __init__(self, first, last, birthdate):
self.first = first
self.last = last
self.birthdate = birthdate
today = dt.date.today()
current_year = today.year
# Will the person still celebrate their birthday this current year?
will_celebrate = birthdate.replace(year=current_year) > today
self.age = current_year - birthdate.year - will_celebrate
@property
def name(self):
return f"{self.first} {self.last}"
Now, if you have been paying attention, you can probably tell there is something wrong with the code above. What?
Just like before, if the attribute birthdate
is updated, then age
is likely to become wrong.
But what is even worse is that the attribute age
can become wrong over time!
If you instantiate the Person
at a given point in time, and then your program runs over day change, or if you serialise your data and then load it back, you might have outdated values.
That is why age
should be a property:
import datetime as dt
class Person:
def __init__(self, first, last, birthdate):
self.first = first
self.last = last
self.birthdate = birthdate
@property
def age(self):
today = dt.date.today()
current_year = today.year
# Will the person still celebrate their birthday this current year?
will_celebrate = self.birthdate.replace(year=current_year) > today
return current_year - self.birthdate.year - will_celebrate
@property
def name(self):
return f"{self.first} {self.last}"
If you want to see some examples of properties that implement attributes that depend dynamically on other attributes of an object, go check the source code for the module pathlib
.
You will find many such usages of @property
there.
For example, path.name
is a property:
# datetime.py, Python 3.11
class PurePath(object):
# ...
@property
def name(self):
"""The final path component, if any."""
print("Computing the name!") # <- Added by me!
parts = self._parts
if len(parts) == (1 if (self._drv or self._root) else 0):
return ''
return parts[-1]
>>> from pathlib import PurePath
>>> p = PurePath("~/Documents/readme.md")
>>> p.name
Computing the name!
'readme.md'
Before I show you another use case for properties, here is a small challenge for you.
Try creating a property hex
for the class Colour
that is shown below.
The property hex
should return a string that starts with #
and that contains the hexadecimal value of the colour.
class Colour:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
If you get it right, you should be able to use the class Colour
like so:
>>> c = Colour(146, 255, 0)
>>> c.hex
'#92ff00'
There is no hard rule for when you should use a property attribute versus a getter method. There seems to be some consensus, however, on some indicators of times when it might be a good idea to use a property:
The more you study and read code from others, the better your appreciation will be for when to use properties. Don't be afraid to try them out, and rest assured that it is not the end of the world if you use a property in a place where you “shouldn't”.
On top of the rules of thumb listed above, if you realise you need any of the functionalities that will be listed next, then that might be an excellent indicator of your need for a property.
One thing that we haven't noted is that the properties that we have defined so far cannot be assigned to.
Let us go back to the Person
class with a name
property:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
@property
def name(self):
return f"{self.first} {self.last}"
If you use this class in your REPL, you will see you cannot assign to name
:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.name = "Charles Smith"
...
AttributeError: ...
Reading from john.name
will run the method name
that returns John's name.
But what would john.name = "Charles Smith"
mean/do..?
While we let that question stew in the back of our minds, we can leverage this AttributeError
for something else:
we can create read-only attributes!
To create a read-only attribute, all you have to do is hide the value you care about behind another attribute (typically, the same attribute preceded by an underscore _
) and then use a property to read from that private attribute.
For example, this is how you can make the attributes first
and last
in the class Person
private:
There is an obvious limitation to this method, though. If the user goes digging, they will figure out that the property is just a thin wrapper around another attribute, and that attribute can be changed:
>>> john = Person("John", "Doe")
>>> john.first = "Charles"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: property 'first' of 'Person' object has no setter
>>> john._first = "Charles"
>>> john.first
'Charles'
This is ok, though. Python is a language for consenting adults. By creating a property that sets a read-only attribute, users should understand that they are not supposed to be going around and changing those attributes, and that making those changes could have unintended consequences. However, if they still choose to change the underlying private attributes, they do so at their own risk.
This pattern for read-only attributes can also be found in the standard library, for example in the class Fraction
of the module fractions
:
# fractions.py, Python 3.11
class Fraction(numbers.Rational):
# ...
@property
def numerator(a):
return a._numerator
As we can see, the attribute numerator
is actually a read-only property:
>>> from fractions import Fraction
>>> f = Fraction(1, 2)
>>> f.numerator
1
>>> f.numerator = 3
...
AttributeError: ...
However, we can set the value of the “private” attribute _numerator
:
>>> f = Fraction(1, 2)
>>> f._numerator = 3
>>> f.numerator
3
>>> f
Fraction(3, 2)
So far, we have used properties only to read attributes from objects. However, properties can also be used to provide more control over how we set attributes.
To go back to our Person
example, what if we wanted the user to also be able to set name
?
If we did that, we'd need to split the name to fetch the first and last names and then update the corresponding attributes.
So, we do that.
We will start by providing this behaviour in a method set_name
:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
@property
def name(self):
return f"{self.first} {self.last}"
def set_name(self, name):
first, last = name.split()
self.first = first
self.last = last
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.set_name("Charles Smith")
>>> john.first
'Charles'
>>> john.name
'Charles Smith'
Now, what we want is to be able to move the functionality of set_name
to the direct assignment into name
, which doesn't work right now:
>>> john.name = "Anne Johnson"
...
AttributeError: property 'name' of 'Person' object has no setter
Notice that Python specifically says that the property name
has no setter.
To turn the method set_name
into a property setter, we just need to do two things:
@attribute.setter
decorator.In the above, “attribute” should actually match the name of the property.
So, in our case, we'll use the decorator @name.setter
:
The module urllib
from the standard library also follows a similar pattern.
The object Request
contains a property full_url
that can be set via a property setter:
# urllib/request.py, Python 3.11
class Request:
# ...
@full_url.setter
def full_url(self, url):
# unwrap('<URL:type://host/path>') --> 'type://host/path'
self._full_url = unwrap(url)
self._full_url, self.fragment = _splittag(self._full_url)
self._parse()
As we can see, urllib
uses a property setter so that changing the value of full_url
updates a few related things:
_full_url
and fragment
;_parse()
which does further work under the hood!In general, we do not define setter methods like set_name
in Python.
If you need to control how an attribute is set, use a property and the setter
decorator.
This can be useful to validate the new value, to normalise it, or to further process it, as shown by the examples for Person.name
and Request.full_url
.
For a second challenge, go back to the example for the property Colour.hex
and try defining a setter for it.
The setter should make this interaction possible:
>>> c = Colour(146, 255, 0)
>>> c.hex
'#92ff00'
>>> c.hex = "#ff00ff"
>>> c.r, c.g, c.b
(255, 0, 255)
To conclude this discussion on property setters, let us go back to the AttributeError
exception that was raised when we first tried to set the value of name
to something else:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.name = "Charles Smith"
...
AttributeError: property 'name' of 'Person' object has no setter
When you see this AttributeError
, you should immediately know that this means you tried to set the value of a property that:
If you are messing with your own code, it probably means you forgot to define the setter via the @xxx.setter
attribute.
If you are using a module you installed, it probably means you are dealing with an attribute that is supposed to be read-only.
Property attributes can have one more functionality associated with them, and that's the third behaviour that is associated with general attributes. In fact, attributes can be:
Naturally, property attributes can also be deleted if you specify a deleter method.
By default, you get an AttributeError
if you try to delete a property with the statement del instance.attribute
:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> del john.name
...
AttributeError: property 'name' of 'Person' object has no deleter
Just like with the setter, you can define a deleter by using the decorator @xxx.deleter
on a method that does the necessary deleting.
For example, the class Person
below defines a simple property attribute first
with a getter, a setter, and a deleter:
class Person:
def __init__(self, first):
self._first = first
@property
def first(self):
return self._first
@first.setter
def first(self, first_name):
self._first = first_name.strip().capitalize()
@first.deleter
def first(self):
del self._first
>>> john = Person("John")
>>> john.first
'John'
>>> john.first = "CHArles"
>>> john.first
'Charles'
>>> del john.first
>>> john.first
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/rodrigogs/Documents/properties.py", line 43, in first
return self._first
^^^^^^^^^^^
AttributeError: 'Person' object has no attribute '_first'. Did you mean: 'first'?
As you can see, when we try to access john.first
after deleting john.first
, we get an AttributeError
.
However, it may look like a confusing error, since we accessed the attribute first
, Python complained it doesn't know about an attribute _first
, and then asked if we were talking about first
...
The issue here was that the deleter only deleted the private attribute _first
but it didn't get rid of the attribute property itself.
Because of this intricacy, the deleter is usually used when we may need to clean up other attributes that are auxiliary to the main property attribute. For example, if you use other private attributes to cache information about your property, you'd clear those in the deleter.
As an example, we can take a look at the Request.full_url
property from the module urllib
again:
# urllib/request.py, Python 3.11
class Request:
# ...
@full_url.setter
def full_url(self, url):
# unwrap('<URL:type://host/path>') --> 'type://host/path'
self._full_url = unwrap(url)
self._full_url, self.fragment = _splittag(self._full_url)
self._parse()
@full_url.deleter
def full_url(self):
self._full_url = None
self.fragment = None
self.selector = ''
Notice how the deleter cleans up the attributes _full_url
and fragment
, which were set inside the setter.
The selector
, which is also cleared inside the deleter, was set inside the call to _parse
.
Before we conclude this Pydon't, let us take a look at the nature of the built-in property
.
We have used property
as a decorator, so far, but is property
really a decorator..?
The documentation shows the built-in property
used in a different way:
# Pasted from the docs.
class C:
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.")
The definition of the property attribute x
, seen above, is equivalent to the definition seen below, which uses the syntax we saw before:
# Using the syntax we have seen.
class C:
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
Well, it is not entirely equivalent. The first definition, pasted from the docs, defines methods that can be used directly:
# Using C from the docs.
>>> c = C()
>>> c.setx(3)
>>> c.getx()
3
As a real-life example of this behaviour, the module calendar
does something similar:
# calendar.py, Python 3.11
class Calendar(object):
"""
Base calendar class. This class doesn't do any formatting. It simply
provides data to subclasses.
"""
def __init__(self, firstweekday=0):
self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
def getfirstweekday(self):
return self._firstweekday % 7
def setfirstweekday(self, firstweekday):
self._firstweekday = firstweekday
firstweekday = property(getfirstweekday, setfirstweekday)
This shows that if you want to define getter/setter methods that you want the user to have access to, you can use property
in an assignment like the one you saw above.
This is not a very common thing to do, though.
This does show, however, that there is a mechanism that is even more powerful than property
in Python.
In fact, if you look closely, all we did in the example for the classes C
and Calendar
, was assign to a name (C.x
and Calendar.firstweekday
).
From that, Python is able to customise the behaviours for reading from, writing to, and deleting that same attribute!
Doesn't that sound insane..?
It may sound insane the first time you hear about it, but it is all thanks to descriptors. Descriptors are a really neat topic that will be covered next!
Here's the main takeaway of this Pydon't, for you, on a silver platter:
“
property
is the Pythonic interface for adding dynamic behaviour to your interactions with attributes in classes.”
This Pydon't showed you that:
property
will turn a method into a property attribute of the same name;@xxx.setter
;property
isn't really a decorator, but a descriptor (whatever that may mean); andIf you liked this Pydon't be sure to leave a reaction below and share this with your friends and fellow Pythonistas. Also, don't forget to subscribe to the newsletter so you don't miss a single Pydon't!
+35 chapters. +400 pages. Hundreds of examples. Over 30,000 readers!
My book “Pydon'ts” teaches you how to write elegant, expressive, and Pythonic code, to help you become a better developer. >>> Download it here 🐍🚀.
property
, https://docs.python.org/3/library/functions.html#property [last accessed 14-05-2023];@property
, @staticmethod
and @classmethod
from scratch, https://tushar.lol/post/descriptors/ [last accessed 11-05-2023];