Extending a Property in a Subclass In Python
Problem
Within a subclass, you want to extend the functionality of a property defined in a parent class.
Solution
class Person:
def __init__(self, name):
self.name = name
# Getter function
@property
def name(self):
return self._name
# Setter function
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._name = value
# Deleter function
@name.deleter
def name(self):
raise AttributeError("Can't delete attribute")
Here is an example of a class that inherits from Person and extends the name property
with new functionality:
class SubPerson(Person):
@property
def name(self):
print('Getting name')
return super().name
@name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
@name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
Here is an example of the new class in use:
>>> s = SubPerson('Guido')
Setting name to Guido
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
Setting name to Larry
>>> s.name = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 16, in name
raise TypeError('Expected a string')
TypeError: Expected a string
>>>
If you only want to extend one of the methods of a property, use code such as the following:
class SubPerson(Person):
@Person.name.getter
def name(self):
print('Getting name')
return super().name
Or, alternatively, for just the setter, use this code:
class SubPerson(Person):
@Person.name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
Discussion
Extending a property in a subclass introduces a number of very subtle problems related to the fact that a property is defined as a collection of getter, setter, and deleter methods, as opposed to just a single method. Thus, when extending a property, you need to figure out if you will redefine all of the methods together or just one of the methods. In the first example, all of the property methods are redefined together. Within each method, super() is used to call the previous implementation. The use of super(Sub Person, SubPerson).name.__set__(self, value) in the setter function is no mis‐ take. To delegate to the previous implementation of the setter, control needs to pass through the __set__() method of the previously defined name property. However, the only way to get to this method is to access it as a class variable instead of an instance variable. This is what happens with the super(SubPerson, SubPerson) operation. If you only want to redefine one of the methods, it’s not enough to use @property by itself. For example, code like this doesn’t work:
class SubPerson(Person):
@property # Doesn't work
def name(self):
print('Getting name')
return super().name
If you try the resulting code, you’ll find that the setter function disappears entirely:
>>> s = SubPerson('Guido')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 5, in __init__
self.name = name
AttributeError: can't set attribute
>>>
Instead, you should change the code to that shown in the solution:
class SubPerson(Person):
@Person.getter
def name(self):
print('Getting name')
return super().name
When you do this, all of the previously defined methods of the property are copied, and the getter function is replaced. It now works as expected:
>>> s = SubPerson('Guido')
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
>>> s.name
Getting name
'Larry'
>>> s.name = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 16, in name
raise TypeError('Expected a string')
TypeError: Expected a string
>>>
In this particular solution, there is no way to replace the hardcoded class name Per son with something more generic. If you don’t know which base class defined a property, you should use the solution where all of the property methods are redefined and su per() is used to pass control to the previous implementation. It’s worth noting that the first technique shown in this recipe can also be used to extend a descriptor, as described in Recipe 8.9. For example:
# A descriptor
class String:
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
instance.__dict__[self.name] = value
# A class with a descriptor
class Person:
name = String('name')
def __init__(self, name):
self.name = name
# Extending a descriptor with a property
class SubPerson(Person):
@property
def name(self):
print('Getting name')
return super().name
@name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
@name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
print('Getting name')
return super().name
@name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
@name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
Finally, it’s worth noting that by the time you read this, subclassing of setter and deleter
methods might be somewhat simplified. The solution shown will still work, but the bug
reported at Python’s issues page might resolve into a cleaner approach in a future Python
version