16.1. Properties¶
The BankAccount class that we used in Chapter 15 marked its _balance attribute as private by placing an underscore at the start of its name. But remember that designating an attribute as private is only a convention: all attributes in Python are technically public, meaning they’re accessible to code outside the class. There’s nothing to prevent code from intentionally or maliciously changing the _balance attribute to an invalid value.
But you can prevent accidental invalid changes to these private attri- butes with properties. In Python, properties are attributes that have specially assigned getter, setter, and deleter methods that can regulate how the attribute is read, changed, and deleted. For example, if the attribute is only supposed to have integer values, setting it to the string ‘42’ will likely cause bugs. A property would call the setter method to run code that fixes, or at least pro- vides early detection of, setting an invalid value. If you’ve thought, “I wish I could run some code each time this attribute was accessed, modified with an assignment statement, or deleted with a del statement,” then you want to use properties. ## Turning an Attribute into a Property First, let’s create a simple class that has a regular attribute instead of a prop- erty. Open a new file editor window and enter the following code, saving it as regularAttributeExample.py:
>>> class ClassWithRegularAttributes:
>>> def __init__(self, someParameter):
>>> self.someAttribute = someParameter
>>> obj = ClassWithRegularAttributes('some initial value')
>>> print(obj.someAttribute) # Prints 'some initial value'
>>> obj.someAttribute = 'changed value'
>>> print(obj.someAttribute) # Prints 'changed value'
>>> del obj.someAttribute # Deletes the someAttribute attribute.
some initial value
changed value
This ClassWithRegularAttributes class has a regular attribute named someAttribute . The__init__() method sets someAttribute to ‘some initial value’ , but we then directly change the attribute’s value to ‘changed value’ . When you run this program, the output looks like this:
some initial value changed value
This output indicates that code can easily change someAttribute to any value. The downside of using regular attributes is that your code can set the someAttribute attribute to invalid values. This flexibility is simple and conve- nient, but it also means someAttribute could be set to some invalid value that causes bugs. Let’s rewrite this class using properties by following these steps to do this for an attribute named someAttribute : 1. Rename the attribute with an underscore prefix: _someAttribute . 2. Create a method named someAttribute with the @property decorator. This getter method has the self parameter that all methods have. 3. Create another method named someAttribute with the @someAttribute.setter decorator. This setter method has parameters named self andvalue . 4. Create another method named someAttribute with the @someAttribute.deleter decorator. This deleter method has the self parameter that all methods have.
Open a new file editor window and enter the following code, saving it as propertiesExample.py:
>>> class ClassWithProperties:
>>> def __init__(self):
>>> self.someAttribute = 'some initial value'
>>> @property
>>> def someAttribute(self): # This is the "getter" method.
>>> return self._someAttribute
>>> @someAttribute.setter
>>> def someAttribute(self, value): # This is the "setter" method.
>>> self._someAttribute = value
>>> @someAttribute.deleter
>>> def someAttribute(self): # This is the "deleter" method.
>>> del self._someAttribute
>>> obj = ClassWithProperties()
>>> print(obj.someAttribute) # Prints 'some initial value'
>>> obj.someAttribute = 'changed value'
>>> print(obj.someAttribute) # Prints 'changed value'
>>> del obj.someAttribute # Deletes the _someAttribute attribute.
some initial value
changed value
This program’s output is the same as the regularAttributeExample.py code, because they effectively do the same task: they print an object’s initial attri- bute and then update that attribute and print it again.
But notice that the code outside the class never directly accesses the _someAttribute attribute (it’s private, after all). Instead, the outside code accesses the someAttribute property. What this property actually consists of is a bit abstract: the getter, setter, and deleter methods combined make up the property. When we rename an attribute named someAttribute to _someAttribute while creating getter, setter, and deleter methods for it, we call this the someAttribute property.
In this context, the _someAttribute attribute is called a backing field or backing variable and is the attribute on which the property is based. Most, but not all, properties use a backing variable. We’ll create a property with- out a backing variable in “Read-Only Properties” later in this chapter. You never call the getter, setter, and deleter methods in your code because Python does it for you under the following circumstances:
When Python runs code that accesses a property, such as print(obj. someAttribute) , behind the scenes, it calls the getter method and uses the returned value.
When Python runs an assignment statement with a property, such as obj.someAttribute = ‘changed value’ , behind the scenes, it calls the setter method, passing the ‘changed value’ string for the value parameter.
When Python runs a del statement with a property, such as del obj.someAttribute , behind the scenes, it calls the deleter method.
The code in the property’s getter, setter, and deleter methods acts on the backing variable directly. You don’t want the getter, setter, or deleter methods to act on the property, because this could cause errors. In one pos- sible example, the getter method would access the property, causing the get- ter method to call itself, which makes it access the property again, causing it to call itself again, and so on until the program crashes. Open a new file edi- tor window and enter the following code, saving it as badPropertyExample.py:
- class ClassWithBadProperty:
- def __init__(self):
self.someAttribute = 'some initial value'
@property def someAttribute(self): # This
# We forgot the _ underscore in self._someAttribute here, causing # us to use the property and call the getter method again:
return self.someAttribute ## This calls the getter again!
@someAttribute.setter def someAttribute(self, value):
self._someAttribute = value# This is the "setter" method.
obj = ClassWithBadProperty() print(obj.someAttribute) # Error because the getter calls the getter.
When you run this code, the getter continually calls itself until Python raises a RecursionError exception:
Traceback (most recent call last): File "badPropertyExample.py", line 16, in <module> print(obj.someAttribute) # Error because the getter calls the getter. File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # This calls the getter again! File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # This calls the getter again! File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # This calls the getter again! [Previous line repeated 996 more times] RecursionError: maximum recursion depth exceeded
To prevent this recursion, the code inside your getter, setter, and del- eter methods should always act on the backing variable (which should have an underscore prefix in its name), never the property. Code outside these methods should use the property, although as with the private access under- score prefix convention, nothing prevents you from writing code on the backing variable anyway. ## Using Setters to Validate Data The most common need for using properties is to validate data or to make sure it’s in the format you want it to be in. You might not want code outside the class to be able to set an attribute to just any value; this could lead to bugs. You can use properties to add checks that ensure only valid values are assigned to an attribute. These checks let you catch bugs earlier in code development, because they raise an exception as soon as an invalid value is set.
Let’s update the wizcoin.py file from Chapter 15 to turn the galleons , sickles , and knuts attributes into properties. We’ll change the setter for these properties so only positive integers are valid. Our WizCoin objects represent an amount of coins, and you can’t have half a coin or an amount of coins less than zero. If code outside the class tries to set the galleons , sickles , or knuts properties to an invalid value, we’ll raise a WizCoinException exception.
Open the wizcoin.py file that you saved in Chapter 15 and modify it to look like the following:
class WizCoinException(Exception):
# """The wizcoin module raises this when the module is misused.""" pass class WizCoin: def __init__(self, galleons, sickles, knuts): # """Create a new WizCoin object with galleons, sickles, and knuts."""
self.galleons = galleons self.sickles = sickles self.knuts = knuts # NOTE: __init__() methods NEVER have a return statement. --snip--
@property def galleons(self): """Returns the number of galleon coins in this object.""" return self._galleons @galleons.setter
def galleons(self, value):
if not isinstance(value, int):
raise WizCoinException('galleons attr must be set to an int, not a ' + value.__class__.__qualname__)
if value < 0: raise WizCoinException('galleons attr must be a positive int, not ' + value.__class__.__qualname__) self._galleons = value --snip--
The new changes add a WizCoinException class 1 that inherits from Python’s built-in Exception class. The class’s docstring describes how the wizcoin module 2 uses it. This is a best practice for Python modules: the WizCoin class’s objects can raise this when they’re misused. That way, if a WizCoin object raises other exception classes, like ValueError or TypeError , this will mostly likely signify that it’s a bug in the WizCoin class.
In the __init__() method, we set the self.galleons , self.sickles , and self.knuts properties 3 to the corresponding parameters.
At the bottom of the file, after the total() and weight() methods, we add a getter 4 and setter method 5 for the self._galleons attribute. The getter simply returns the value in self._galleons . The setter checks whether the value being assigned to the galleons property is an integer 6 and posi- tive 8. If either check fails, WizCoinException is raised with an error message. This check prevents _galleons from ever being set with an invalid value as long as code always uses the galleons property.
All Python objects automatically have a __class__ attribute, which refers to the object’s class object. In other words, value.__class__ is the same class object that type(value) returns. This class object has an attribute named __qualname__ that is a string of the class’s name. (Specifically, it’s the qualified name of the class, which includes the names of any classes the class object is nested in. Nested classes are of limited use and beyond the scope of this book.) For example, if value stored the date object returned by datetime. date(2021, 1, 1) , then value.__class__.__qualnamewould be the string ‘date’ . The exception messages use value.__class.__qualname__ 7 to get a string of the value object’s name. The class name makes the error message more useful to the programmer reading it, because it identifies not only that the value argument was not the right type, but what type it was and what type it should be.
You’ll need to copy the code for the getter and setter for _galleons to use for the _sickles and _knuts attributes as well. Their code is identical except they use the _sickles and _knuts attributes, instead of _galleons , as backing variables. ## Read-Only Properties Your objects might need some read-only properties that can’t be set with the assignment operator = . You can make a property read-only by omitting the setter and deleter methods.
For example, the total() method in the WizCoin class returns the value of the object in knuts. We could change this from a regular method to a read-only property, because there is no reasonable way to set the total of a WizCoin object. After all, if you set total to the integer 1000 , does this mean 1,000 knuts? Or does it mean 1 galleon and 493 knuts? Or does it mean some other combination? For this reason, we’ll make total a read-only prop- erty by adding the code in bold to the wizcoin.py file:
@property def total(self): """Total value (in knuts) of all the coins in this WizCoin object.""" return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts) # Note that there is no setter or deleter method for total.
After you add the @property function decorator in front of total() , Python will call the total() method whenever total is accessed. Because there is no setter or deleter method, Python raises AttributeError if any code attempts to modify or delete total by using it in an assignment or del state- ment, respectively. Notice that the value of the total property depends on the value in the galleons , sickles , and knuts properties: this property isn’t based on a backing variable named _total . Enter the following into the interactive shell:
>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> p
>>> purse.total = 1000
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
You might not like that your program immediately crashes when you attempt to change a read-only property, but this behavior is preferable to allowing a change to a read-only property. Your program being able to mod- ify a read-only property would certainly cause a bug at some point while the program runs. If this bug happens much later after you modify the read-only property, it would be hard to track down the original cause. Crashing imme- diately allows you to notice the problem sooner.
Don’t confuse read-only properties with constant variables. Constant variables are written in all uppercase and rely on the programmer to not modify them. Their value is supposed to remain constant and unchang- ing for the duration of a program’s run. A read-only property is, as with any attribute, associated with an object. A read-only property cannot be directly set or deleted. But it might evaluate to a changing value. Our WizCoin class’s total property changes as its galleons , sickles , and knuts properties change. ## When to Use Properties As you saw in the previous sections, properties provide more control over how we can use a class’s attributes, and they’re a Pythonic way to write code. Methods with names like getSomeAttribute() or setSomeAttribute() signal that you should probably use properties instead.
This isn’t to say that every instance of a method beginning with get or set should immediately be replaced with a property. There are situations in which you should use a method, even if its name begins with get or set. Here are some examples:
For slow operations that take more than a second or two—for example,downloading or uploading a file
For operations that have side effects, such as changes to other attributes or objects
For operations that require additional arguments to be passed to the get or set operation—for example, in a method call like emailObj.getFileAttachment(filename)
Programmers often think of methods as verbs (in the sense that methods perform some action), and they think of attributes and properties as nouns (in the sense that they represent some item or object). If your code seems to be performing more of an action of getting or setting rather than getting or setting an item, it might be best to use a getter or setter method. Ultimately, this decision depends on what sounds right to you as the programmer.
The great advantage of using Python’s properties is that you don’t have to use them when you first create your class. You can use regular attributes, and if you need properties later, you can convert the attributes to properties without breaking any code outside the class. When we make a property with the attribute’s name, we can rename the attribute using a prefix underscore and our program will still work as it did before.