Customizing what happens when you assign an attribute

Share
Copied to clipboard.
Series: Properties
Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read Watch as video Python 3.8—3.12
Python Morsels
Watch as video
03:43

How can you customize what happens when you assign to a specific attribute on a Python class?

Accessing and updating attributes on a class

Here we have a class called Person:

class Person:
    def __init__(self, name, location):
        self.name = name
        self.location = location

Person objects have a name attribute and a location attribute:

>>> trey = Person("Trey", "San Diego")
>>> trey.name
'Trey'
>>> trey.location
'San Diego'

We want to make it so that whenever we assign to the location attribute all previous values of the location attribute would be stored somewhere.

>>> trey.location = "Portland"

We'd like to make a past_locations attribute which would show us all previous values for location on a specific Person object:

>>> trey.past_locations

In many programming languages, like Java, the solution to this problem is getter methods and setter methods.

So instead of having a location attribute, we would have a private attribute. Python doesn't have private attributes, but sometimes we put an underscore (_) before an attribute name to note that it's private by convention.

Here's an updated version of our Person class with a getter and setter method and a private (by convention) _location attribute:

class Person:
    def __init__(self, name, location):
        self.name = name
        self.past_locations = []
        self.set_location(location)

    def get_location(self):
        return self._location

    def set_location(self, location):
        self._location = location
        self.past_locations.append(location)

On this new class, instead of accessing location directly (which doesn't work anymore) we would call the get_location method:

>>> trey = Person("Trey", "San Diego")
>>> trey.get_location()
'San Diego'

And to set a location we'd call the set_location method:

>>> trey.set_location("Portland")
>>> trey.get_location()
'Portland'

The benefit of getter and setter methods is that we can hook into these methods, putting any code we'd like inside them.

For example in our set_location method, we're appending to the past_locations list:

    def set_location(self, location):
        self._location = location
        self.past_locations.append(location)

So that past_locations attribute now shows all locations we've ever set for this Person object:

>>> trey.past_locations
['San Diego', 'Portland']

Using properties to hook into assignment of an attribute

In Python, we don't tend to use getter and setter methods. We don't have a get_name, and a set_name, and a get_location, and a set_location for every single attribute. Instead, we tend to just assign the attributes and read from attributes as we like.

But in this particular situation (where we have an attribute and we'd like to change how it works) we're kind of stuck. We need some way to hook into the assignment of that attribute. Fortunately, in Python, there is a way to do this: we can use a property.

Here's our modified Person class with properties:

class Person:
    def __init__(self, name, location):
        self.name = name
        self.past_locations = []
        self.location = location

    @property
    def location(self):
        return self._location

    @location.setter
    def location(self, location):
        self._location = location
        self.past_locations.append(location)

Properties allow us to hook into the getting of an attribute.

>>> trey = Person("Trey", "San Diego")
>>> trey.location
'San Diego'

When we access the location attribute now, under the hood it's actually accessing the _location attribute.

But properties also allow us to customize what happens when we assign to a specific attribute.

By default, if we assign to a property we'll get an error. But in our case, we don't get an error:

>>> trey.location = "Portland"
>>> trey.location
'Portland'

It works because we've implemented a setter for our property:

    @location.setter
    def location(self, location):
        self._location = location
        self.past_locations.append(location)

The syntax for property setters is a little bit weird, so I don't recommend memorizing it (just look it up when/if you need it).

Breaking down the syntax for property setters

The property setter syntax starts with the name or our property (location) followed by .setter. We use that as a decorator to decorate a location method (named the same as our property):

    @location.setter
    def location(self, location):
        ...

We accept an argument which represents whatever is assigned to this property (on the right-hand side of the equals sign during assignment).

We're then storing that actual value on _location, and every time our location changes, we're appending each value to our past_location list:

        self._location = location
        self.past_locations.append(location)

We never assign to our private attribute directly

So when we access the past_locations list, both of our locations are reflected:

>>> trey.past_locations
['San Diego', 'Portland']

Any time the location changes, past_locations will be updated.

Notice that even in our initializer, we're assigning to location (rather than _location):

    def __init__(self, name, location):
        self.name = name
        self.past_locations = []
        self.location = location

That's the reason "San Diego" is the first value in this past_locations list: when we assigned to the location attribute in our initializer it called our property setter.

In fact, every time location is assigned to, the setter will be called for this property.

Summary

If you would like to customize what happens when you assign to a specific attribute on your class in Python, you can use a property with a setter.

Series: Properties

We don't use getter and setter methods in Python. Instead we make properties.

Properties allow us to customize what happens when you access an attribute and what happens when you assign to an attribute.

To track your progress on this Python Morsels topic trail, sign in or sign up.

0%
A Python Tip Every Week

Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.

Python Morsels
Watch as video
03:43