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:

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:

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).

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: