Python Properties

It is often considered best practice to create getters and setters for a class's public properties. Many languages allow you to implement this in different ways, either by using a function (like person.getName()), or by using a language-specific get or set construct. In Python, it is done using @property.

In this article I'll be describing they Python property decorator, which you may have seen being used with the @decorator syntax:

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

    @property
    def full_name(self):
        return self.first_name + ' ' + self.last_name

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

    @full_name.deleter
    def full_name(self):
        del self.first_name
        del self.last_name

This is Python's way of creating getters, setters, and deleters (or mutator methods) for a property in a class.

In this case, the @property decorator makes it so you call the full_name(self) method like it is just a normal property, when in reality it is actually a method that contains code to be run when the property is set.

Using a getter/setter/deleter like this provides us with quite a few advantages, a few of which I've listed here:

  • Validation: Before setting the internal property, you can validate that the provided value meets some criteria, and have it throw an error if it doesn't.
  • Lazy loading: Resources can by lazily loaded to defer work until it is actually needed, saving time and resources
  • Abstraction: Getters and setters allow you to abstract out the internal representation of data. Like our example above, for example, the first and last names are stored separately, but the getters and setters contain the logic that uses the first and last names to create the full name.
  • Debugging: Since mutator methods can encapsulate any code, it becomes a great place for interception when debugging (or logging) your code. For example, you could log or inspect each time that a property's value is changed.

Python achieves this functionality with decorators, which are special methods used to change the behavior of another function or class. In order to describe how the @property decorator works, let's take a look at a simpler decorator and how it works internally.

A decorator is simply a function that takes another function as an argument and adding to its behavior by wrapping it. Here is a simple example:

# decorator.py

def some_func():  
    print 'Hey, you guys'

def my_decorator(func):  
    def inner():
        print 'Before func!'
        func()
        print 'After func!'

    return inner

print 'some_func():'  
some_func()

print ''

some_func_decorated = my_decorator(some_func)

print 'some_func() with decorator:'  
some_func_decorated()  

Running this code gives you:

$ python decorator.py
some_func():  
Hey, you guys

some_func() with decorator:  
Before func!  
Hey, you guys  
After func!  

As you can see, the my_decorator() function dynamically creates a new function to return using the input function, adding code to be executed before and after the original function runs.

The property decorator is implemented with a pattern similar to the my_decorator function. Using the Python @decorator syntax, it receives the decorated function as an argument, just like in my example: some_func_decorated = my_decorator(some_func).

So, going back to my first example, this code:

@property
def full_name_getter(self):  
    return self.first_name + ' ' + self.last_name

Is roughly equivalent to this:

def full_name_getter(self):  
    return self.first_name + ' ' + self.last_name

full_name = property(full_name_getter)  

Note that I changed some function names for clarity.

Then, later on when you want to use @full_name.setter as we do in the example, what you're really calling is:

def full_name_setter(self, value):  
    first_name, last_name = value.split(' ')
    self.first_name = first_name
    self.last_name = last_name

full_name = property(full_name_getter)  
full_name = full_name.setter(full_name_setter)  

Now this new full_name object (an instance of the property object) has both getter and setter methods.

In order to use these with our class, Person, the property object acts as a descriptor, which means it has its own __get__(), __set__() and __delete__() methods. The __get__() and __set__() methods are triggered on an object when a property is retrieved or set, and __delete__() is triggered when a property is deleted with del.

So person.full_name = 'Billy Bob' triggers the __set__() method, which was inherited from object. This brings us to an important point - your class must inherit from object in order for this to work. So a class like this would not be able to use setter properties since it doesn't inherit from object:

class Person:  
    pass

Thanks to property, these methods now correspond to our full_name_getter and full_name_setter methods from above:

full_name.fget is full_name_getter    # True  
full_name.fset is full_name_setter    # True  

fget and fset are now wrapped by .__get__() and .__set__(), respectively.

And finally, these descriptor objects can be accessed by passing a reference to our class, Person:

>>> person = Person('Billy', 'Bob')
>>> 
>>> full_name.__get__(person)
Billy Bob  
>>>
>>> full_name.__set__(person, 'Timmy Thomas')
>>>
>>> person.first_name
Timmy  
>>> person.last_name
Thomas  

This is essentially how properties work under the surface.