Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Hint: You can adjust the default video playback speed in your account settings.
Hint: You can set your subtitle preferences in your account settings.
Sorry! Looks like there’s an issue with video playback 🙁 This might be due to a temporary outage or because of a configuration issue with your browser. Please see our video player troubleshooting guide to resolve the issue.

Properties and Descriptors

00:00 In the previous lesson, you learned more about Python attributes. In this lesson, you’ll learn other ways of accessing an object’s content. Say you wanted to have a method, but you wanted it to look like an attribute.

00:13 Well, you can do that by wrapping the method with the @property decorator. This allows you to do a calculation each time an attribute is referenced.

00:22 If you like, let me go show you an example.

00:27 New REPL, new class. This time I’m building a person.

00:32 .__init__() takes two arguments, the first and last name of my person. And

00:42 there’s the boilerplate for storing .first and .last on the actual object.

00:53 The .full_name() method is wrapped with the @property decorator, which means you can access it like an attribute. Properties must not take any arguments besides self.

01:04 This makes sense. If you’re accessing it as an attribute, you have no way of passing arguments in.

01:11 Inside the method, whatever I return will be the property’s value. Here I’m concatenating the first and last name together. Note how I’m accessing those attributes using self inside the f-string.

01:24 This method is horribly Western-centric. The whole concept of first and last name is very European. Google falsehoods programmers believe about names for an education on these biases, or look up the RadioLab podcast episode called “Null” about the problems people with that last name have in the modern world. Anyhow, I’ll step off my soapbox.

01:47 That’s our class. Let’s create a person.

01:55 See what I mean? It isn’t like "of Rivia" is actually his last name. Let’s access the first name attribute, then the last name, and now the full name.

02:10 It’s just like any other attribute. The property hides that this is a method from the user. Remember in a previous lesson when I commented on Asciimatic’s use of private values?

02:23 Well, that codebase uses properties as the public interface. Almost all attributes have underscores, while the property exposes the actual value. In some cases, this might just be a developer quirk. Hey, you all have them.

02:38 Notice I said you. I have no quirks at all. My foibles are severe enough to be promoted to full-blown aberrations. What was I saying? Right, developer quirk.

02:48 One reason for doing this has to do with caching. Say you had a value that had some complications to calculate. You could initialize it to None, and then the first time someone used it, do the calculation.

03:01 Then you cache an internal copy, and on subsequent accesses, use that cached copy. You can sort of do this with a method, but as a property, you’re hiding away this implementation detail.

03:14 The @property decorator is part of a deeper mechanism called the descriptor protocol. So far, you’ve just seen the simplest portion: using @property to access a value.

03:25 You can use a similar mechanism for trapping the setting of a value. To do this, you decorate the method with the name of the attribute, then .setter. This mechanism allows you to perform side effects when a value is set, for example. You can perform other calculations, update that cache I was talking about, or do error checks on the value.

03:48 All of these things could equally be done with a method, and in fact, in a lot of other languages, having getter and setter methods is common. This really is just syntactical sugar, but it can make your code a little more readable.

04:01 Rather than having .get_radius() and .set_radius(), you just have .radius, and your code hides away what checking you need to do when the value is assigned.

04:11 A common pattern you’ll find in code is to pass a value to .__init__() that gets stored in the class. Have that value stored as a non-public attribute, and then have a property with the same name but without the underscore and then a corresponding setter for that value. Inside the setter, you’re actually setting the non-public attribute as well as doing any error checking or other work, but from the perspective of the programmer using the object, it just looks like an attribute.

04:41 I’ve already told a couple of anecdotes relating to this. I’ll wait until at least the next lesson before I start repeating myself. Let’s go use a setter.

04:51 Here’s a new version of my Circle class using ._radius as a non-public attribute and a property named radius. This would be that pattern I just mentioned.

05:08 And this is what a setter decorator looks like. Note that it uses the same name as the property that you’re setting and then the .setter. Remember, the property method takes no additional arguments. Well, the setter does need an argument: the new value being set.

05:26 If you’re used to Python, you might notice there’s something a little weird going on here with the name. Both the property and the setter methods are named radius. Normally, this wouldn’t be allowed in Python.

05:37 Python doesn’t support method overloading in the same class. But decorators are kind of a special case. Whenever you see a decorator, it’s actually wrapping the thing you’re decorating with a function.

05:48 Although it says @radius here, from the compiler’s perspective, it’s seeing something completely different. As weird as this is, it has become a common pattern.

05:57 It sort of hits home the fact that this is a setter for radius. You can call the method anything. Because it’s wrapped, it doesn’t really matter. Inside the method, I’m going to use the setter to do a little bit of an error checking. Here, I’m checking whether or not the contents of .value is either an int or a float and is a positive number.

06:20 If you haven’t come across the notation with the pipe operator before, it was added in Python 3.10. It saves some typing. In the old days, you’d have to or together two calls to isinstance(). Isn’t progress grand?

06:34 Well, if the value being set isn’t an int or a float or it’s less than zero, I raise a ValueError to tell them they can’t do that. If everything is okay, then I store the value on the non-public attribute. Let’s create a circle.

06:53 Once more, a small circle.

06:57 And there’s my radius. From the outside, it looks no different than the original code, but remember, it’s actually using a property here.

07:08 Just like a regular attribute, I can assign my radius,

07:12 and it takes on the new value. Of course, in this case, assigning the new value actually called the setter. How do I prove that? Well, by triggering the error check, of course.

07:28 Again, you can do this with methods, but this is so very clean. Programmers using Circle don’t have to think about it. They can just use the radius naturally, and yet you still get all the advantages of a method that checks for illegal values. Okay, you’ve seen a bunch having to do with attributes.

07:47 Now, let’s add to your knowledge of methods.

Erick Kendall on Sept. 7, 2023

I’m initializing the class with def __init__(self, radius) yet I have def radius(self,value) - I’m not clear what’s happening when I create the class small = Circle(3) - it’s as if I have two competing initializers

Christopher Trudeau RP Team on Sept. 7, 2023

Hi Erik,

Without seeing the code you’re trying, I can only guess, but this one is a bit tricky. Make sure you have the leading underscore on self._radius in __init__, otherwise you’ll be calling the setter.

>>> class Circle:
...     def __init__(self, radius):
...         self._radius = radius   # underscore is important here!
...     @property
...     def radius(self):
...         return self._radius    # value created in __init__
...     @radius.setter
...     def radius(self, value):
...         if not isinstance(value, int | float) or value <= 0:
...             raise ValueError("Radius must be a positive number")
...         self._radius = value

There are two ways of using the descriptor protocol, the first calls the setter in __init__ and the second (the one shown here) uses a different variable referenced by both __init__ and the getter/setter methods.

In fact there is a drawback to the method shown here – it only checks for a valid radius in the setter, not in the initializer. You could do Circle('x') and it would allow it.

IIRC, the first method is covered in part 2 of the course, soon to be released.

If this isn’t the problem you’re encountering, paste your code in response, and I can see what I can figure out.

Erick Kendall on Sept. 7, 2023

Yes, this was the code I was referring to. I completely missed the underscore in __init__. Thank you!

Christopher Trudeau RP Team on Sept. 8, 2023

Yep, easy enough mistake. In fact when reading one of the other authors’ code when building the course I reached out to him telling him he missed it… not realizing until afterwards he was trying to do the other structure which explicitly uses the setter. This can be sneaky.

Become a Member to join the conversation.