Making a read-only attribute

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.

0%
Watch other topic trails

Transcript

How can we make a read-only attribute in Python?

A property is like an auto-updating attribute

We have a class called Square which accepts a length and an optional color and stores those as length and color attributes:

class Square:
    def __init__(self, length, color=None):
        self.length = length
        self.color = color

    def __repr__(self):
        return f"Square({self.length!r}, {self.color!r})"

    @property
    def area(self):
        return self.length**2

If we make a Square object (with just a length in this case):

>>> s1 = Square(4)

We can see the length and color in the string representation of this Square object:

>>> s1
Square(4, None)

This class also has an area property.

    @property
    def area(self):
        return self.length**2

A property is a kind of like a virtual attribute.

Every time we access the area attribute, a function will be executed and its return value will be given back to us:

>>> s1.area
16

That function call happens automatically, simply by accessing the area attribute.

So if we change the length of a Square object:

>>> s1.length = 3

The area will seem to change automatically:

>>> s1.area
9

Properties can make read-only attributes

What if we wanted to make it so the color of our Square objects can be changed but the length and area can't be changed?

>>> s1.color = 'purple'
>>> s1.color
'purple'

We want to make the length and the area attributes read-only.

It turns out the area attribute is already read-only:

>>> s1.area = 100
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

We can't assign to area because properties are read-only by default.

We could try to make our length attribute into a property, by adding this to our class definition:

    @property
    def length(self):
        return self.length

We're using the property decorator to create a property named length and this property returns the length attribute when accessed.

This length property doesn't quite work though:

>>> s1 = Square(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/trey/shapes.py", line 3, in __init__
    self.length = length
AttributeError: can't set attribute

It doesn't work because in our initializer method we assigned to length:

    def __init__(self, length, color=None):
        self.length = length
        self.color = color

But length is a property that doesn't have a setter, so it's read-only (all properties are read-only by default).

Our property is accessing data that's stored in the same places as our property. That's not possible! There's nothing to distinguish our property name from an attribute of the same name, so we have a name collision.

We need our property name to be different from the attribute where we actually store our data. We don't want to rename our property, so let's rename our attribute.

Using an underscore prefix for internal attributes

We need an attribute that has a different name than length.

When you have an attribute that represents internal data that shouldn't be accessed directly (by code outside of your class) it's common in Python to prefix a single underscore (_) before that attribute name.

PEP 8 notes this convention as well: "Use one leading underscore only for non-public methods and instance variables."

So we'll rename the attribute that stores our length to _length: So, we'll say:

    def __init__(self, length, color=None):
        self._length = length
        self.color = color

And our property will access self._length now to get the length:

    @property
    def length(self):
        return self._length

Here's the full class:

class Square:
    def __init__(self, length, color=None):
        self._length = length
        self.color = color

    def __repr__(self):
        return f"Square({self.length!r}, {self.color!r})"

    @property
    def area(self):
        return self.length**2

    @property
    def length(self):
        return self._length

Now when we make a Square object, we'll see that there is a length:

>>> s1 = Square(4)
>>> s1.length
4

But we can't assign to it because it's a property:

>>> s1.length = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

The single underscore prefix is just a convention

Note that this single underscore prefix is just a convention. So we can still access the _length attribute if we want to:

>>> s1._length
4

And in fact we can even change it:

>>> s1._length = 5

Which changes the length on our Square object:

>>> s1.length
5

But when other Python programmers see an assignment to an underscore-prefixed attribute, they'll look at that code and think something strange is going on: we're changing some internal details of an object. An underscore-prefixed attribute is supposed to be private by convention, so it's uncommon to see an assignment to an underscore-prefixed attribute.

Summary

If you need to make a read-only attribute in Python, you can turn your attribute into a property that delegates to an attribute with almost the same name, but with an underscore prefixed before the its name to note that it's private convention.