Python's property(): Add Managed Attributes to Your Classes

Python's property(): Add Managed Attributes to Your Classes

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Managing Attributes With Python's property()

With Python’s property(), you can create managed attributes in your classes. You can use managed attributes, also known as properties, when you need to modify their internal implementation without changing the public API of the class. Providing stable APIs can help you avoid breaking your users’ code when they rely on your classes and objects.

Properties are arguably the most popular way to create managed attributes quickly and in the purest Pythonic style.

In this tutorial, you’ll learn how to:

  • Create managed attributes or properties in your classes
  • Perform lazy attribute evaluation and provide computed attributes
  • Avoid setter and getter methods to make your classes more Pythonic
  • Create read-only, read-write, and write-only properties
  • Create consistent and backward-compatible APIs for your classes

You’ll also write a few practical examples that use property() for validating input data, computing attribute values dynamically, logging your code, and more. To get the most out of this tutorial, you should know the basics of object-oriented programming and decorators in Python.

Managing Attributes in Your Classes

When you define a class in an object-oriented programming language, you’ll probably end up with some instance and class attributes. In other words, you’ll end up with variables that are accessible through the instance, class, or even both, depending on the language. Attributes represent or hold the internal state of a given object, which you’ll often need to access and mutate.

Typically, you have at least two ways to manage an attribute. Either you can access and mutate the attribute directly or you can use methods. Methods are functions attached to a given class. They provide the behaviors and actions that an object can perform with its internal data and attributes.

If you expose your attributes to the user, then they become part of the public API of your classes. Your user will access and mutate them directly in their code. The problem comes when you need to change the internal implementation of a given attribute.

Say you’re working on a Circle class. The initial implementation has a single attribute called .radius. You finish coding the class and make it available to your end users. They start using Circle in their code to create a lot of awesome projects and applications. Good job!

Now suppose that you have an important user that comes to you with a new requirement. They don’t want Circle to store the radius any longer. They need a public .diameter attribute.

At this point, removing .radius to start using .diameter could break the code of some of your end users. You need to manage this situation in a way other than removing .radius.

Programming languages such as Java and C++ encourage you to never expose your attributes to avoid this kind of problem. Instead, you should provide getter and setter methods, also known as accessors and mutators, respectively. These methods offer a way to change the internal implementation of your attributes without changing your public API.

In the end, these languages need getter and setter methods because they don’t provide a suitable way to change the internal implementation of an attribute if a given requirement changes. Changing the internal implementation would require an API modification, which can break your end users’ code.

The Getter and Setter Approach in Python

Technically, there’s nothing that stops you from using getter and setter methods in Python. Here’s how this approach would look:

Python
# point.py

class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    def get_y(self):
        return self._y

    def set_y(self, value):
        self._y = value