Subclassing: Is it possible to override a property with a conventional attribute?

This will be a long winded answer that might only serve to be complimentary... but your question took me for a ride down the rabbit hole so I'd like to share my findings (and pain) as well.

You might ultimately find this answer not helpful to your actual problem. In fact, my conclusion is that - I wouldn't do this at all. Having said that, the background to this conclusion might entertain you a bit, since you're looking for more details.


Addressing some misconception

The first answer, while correct in most cases, is not always the case. For instance, consider this class:

class Foo:
    def __init__(self):
        self.name = 'Foo!'
        @property
        def inst_prop():
            return f'Retrieving {self.name}'
        self.inst_prop = inst_prop

inst_prop, while being a property, is irrevocably an instance attribute:

>>> Foo.inst_prop
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'

It all depends where your property is defined in the first place. If your @property is defined within the class "scope" (or really, the namespace), it becomes a class attribute. In my example, the class itself isn't aware of any inst_prop until instantiated. Of course, it's not very useful as a property at all here.


But first, let's address your comment on inheritance resolution...

So how exactly does inheritance factor into this issue? This following article dives a little bit into the topic, and the Method Resolution Order is somewhat related, though it discusses mostly the Breadth of inheritance instead of Depth.

Combined with our finding, given these below setup:

@property
def some_prop(self):
    return "Family property"

class Grandparent:
    culture = some_prop
    world_view = some_prop

class Parent(Grandparent):
    world_view = "Parent's new world_view"

class Child(Parent):
    def __init__(self):
        try:
            self.world_view = "Child's new world_view"
            self.culture = "Child's new culture"
        except AttributeError as exc:
            print(exc)
            self.__dict__['culture'] = "Child's desired new culture"

Imagine what happens when these lines are executed:

print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

The result is thus:

Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>

Notice how:

  1. self.world_view was able to be applied, while self.culture failed
  2. culture does not exist in Child.__dict__ (the mappingproxy of the class, not to be confused with the instance __dict__)
  3. Even though culture exists in c.__dict__, it is not referenced.

You might be able to guess why - world_view was overwritten by Parent class as a non-property, so Child was able to overwrite it as well. Meanwhile, since culture is inherited, it only exists within the mappingproxy of Grandparent:

Grandparent.__dict__ is: {
    '__module__': '__main__', 
    'culture': <property object at 0x00694C00>, 
    'world_view': <property object at 0x00694C00>, 
    ...
}

In fact if you try to remove Parent.culture:

>>> del Parent.culture
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    del Parent.culture
AttributeError: culture

You will notice it doesn't even exist for Parent. Because the object is directly referring back to Grandparent.culture.


So, what about the Resolution Order?

So we are interested to observe the actual Resolution Order, let's try removing Parent.world_view instead:

del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Wonder what the result is?

c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>

It reverted back to Grandparent's world_view property, even though we had successfully manage to assign the self.world_view before! But what if we forcefully change world_view at the class level, like the other answer? What if we delete it? What if we assign the current class attribute to be a property?

Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

The result is:

# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view

# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view

# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>

This is interesting because c.world_view is restored to its instance attribute, while Child.world_view is the one we assigned. After removing the instance attribute, it reverts to the class attribute. And after reassigning the Child.world_view to the property, we instantly lose access to the instance attribute.

Therefore, we can surmise the following resolution order:

  1. If a class attribute exists and it is a property, retrieve its value via getter or fget (more on this later). Current class first to Base class last.
  2. Otherwise, if an instance attribute exist, retrieve the instance attribute value.
  3. Else, retrieve the non-property class attribute. Current class first to Base class last.

In that case, let's remove the root property:

del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

Which gives:

c.culture is: Child's desired new culture
Traceback (most recent call last):
  File "<pyshell#74>", line 1, in <module>
    print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'

Ta-dah! Child now has their own culture based on the forceful insertion into c.__dict__. Child.culture doesn't exist, of course, since it was never defined in Parent or Child class attribute, and Grandparent's was removed.


Is this the root cause of my problem?

Actually, no. The error you're getting, which we're still observing when assigning self.culture, is totally different. But the inheritance order sets the backdrop to the answer - which is the property itself.

Besides the previously mentioned getter method, property also have a few neat tricks up its sleeves. The most relevant in this case is the setter, or fset method, which is triggered by self.culture = ... line. Since your property didn't implement any setter or fget function, python doesn't know what to do, and throws an AttributeError instead (i.e. can't set attribute).

If however you implemented a setter method:

@property
def some_prop(self):
    return "Family property"

@some_prop.setter
def some_prop(self, val):
    print(f"property setter is called!")
    # do something else...

When instantiating the Child class you will get:

Instantiating Child class...
property setter is called!

Instead of receiving an AttributeError, you are now actually calling the some_prop.setter method. Which gives you more control over your object... with our previous findings, we know that we need to have a class attribute overwritten before it reaches the property. This could be implemented within the base class as a trigger. Here's a fresh example:

class Grandparent:
    @property
    def culture(self):
        return "Family property"
    
    # add a setter method
    @culture.setter
    def culture(self, val):
        print('Fine, have your own culture')
        # overwrite the child class attribute
        type(self).culture = None
        self.culture = val

class Parent(Grandparent):
    pass

class Child(Parent):
    def __init__(self):
        self.culture = "I'm a millennial!"

c = Child()
print(c.culture)

Which results in:

Fine, have your own culture
I'm a millennial!

TA-DAH! You can now overwrite your own instance attribute over an inherited property!


So, problem solved?

... Not really. The problem with this approach is, now you can't have a proper setter method. There are cases where you do want to set values on your property. But now whenever you set self.culture = ... it will always overwrite whatever function you defined in the getter (which in this instance, really is just the @property wrapped portion. You can add in more nuanced measures, but one way or another it'll always involve more than just self.culture = .... e.g.:

class Grandparent:
    # ...
    @culture.setter
    def culture(self, val):
        if isinstance(val, tuple):
            if val[1]:
                print('Fine, have your own culture')
                type(self).culture = None
                self.culture = val[0]
        else:
            raise AttributeError("Oh no you don't")

# ...

class Child(Parent):
    def __init__(self):
        try:
            # Usual setter
            self.culture = "I'm a Gen X!"
        except AttributeError:
            # Trigger the overwrite condition
            self.culture = "I'm a Boomer!", True

It's waaaaay more complicated than the other answer, size = None at the class level.

You could also consider writing your own descriptor instead to handle the __get__ and __set__, or additional methods. But at the end of the day, when self.culture is referenced, the __get__ will always be triggered first, and when self.culture = ... is referenced, __set__ will always be triggered first. There's no getting around it as far as I've tried.


The crux of the issue, IMO

The problem I see here is - you can't have your cake and eat it too. property is meant like a descriptor with convenient access from methods like getattr or setattr. If you also want these methods to achieve a different purpose, you're just asking for trouble. I would perhaps rethink the approach:

  1. Do I really need a property for this?
  2. Could a method serve me any differently?
  3. If I need a property, is there any reason I would need to overwrite it?
  4. Does the subclass really belong in the same family if these property don't apply?
  5. If I do need to overwrite any/all propertys, would a separate method serve me better than simply reassigning, since reassigning can accidentally void the propertys?

For point 5, my approach would be have an overwrite_prop() method in the base class that overwrite the current class attribute so that the property will no longer be triggered:

class Grandparent:
    # ...
    def overwrite_props(self):
        # reassign class attributes
        type(self).size = None
        type(self).len = None
        # other properties, if necessary

# ...

# Usage
class Child(Parent):
    def __init__(self):
        self.overwrite_props()
        self.size = 5
        self.len = 10

As you can see, while still a bit contrived, it is at least more explicit than a cryptic size = None. That said, ultimately, I wouldn't overwrite the property at all, and would reconsider my design from the root.

If you have made it this far - thank you for walking this journey with me. It was a fun little exercise.


A @property is defined at the class level. The documentation goes into exhaustive detail on how it works, but suffice it to say that setting or getting the property resolve into calling a particular method. However, the property object that manages this process is defined with the class's own definition. That is, it's defined as a class variable but behaves like an instance variable.

One consequence of this is that you can reassign it freely at the class level:

print(Math_Set_Base.size)
# <property object at 0x10776d6d0>

Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4

And just like any other class-level name (e.g. methods), you can override it in a subclass by just explicitly defining it differently:

class Square_Integers_Below(Math_Set_Base):
    # explicitly define size at the class level to be literally anything other than a @property
    size = None

    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Square_Integers_Below(4).size)  # 2
print(Square_Integers_Below.size)     # None

When we create an actual instance, the instance variable simply shadows the class variable of the same name. The property object normally uses some shenanigans to manipulate this process (i.e. applying getters and setters) but when the class-level name isn't defined as a property, nothing special happens, and so it acts as you'd expect of any other variable.


A property is a data descriptor which takes precedence over an instance attribute with the same name. You could define a non-data descriptor with a unique __get__() method: an instance attribute takes precedence over the non-data descriptor with the same name, see the docs. The problem here is that the non_data_property defined below is for computation purpose only (you can't define a setter or a deleter) but it seems to be the case in your example.

import math

class non_data_property:
    def __init__(self, fget):
        self.__doc__ = fget.__doc__
        self.fget = fget

    def __get__(self, obj, cls):
        if obj is None:
            return self
        return self.fget(obj)

class Math_Set_Base:
    @non_data_property
    def size(self, *elements):
        return len(self.elements)

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3

However this assumes that you have access to the base class in order to make this changes.


You don't need assignment (to size) at all. size is a property in the base class, so you can override that property in the child class:

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

    # size = property(lambda self: self.elements)


class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._cap = cap

    @property
    def size(self):
        return int(math.sqrt(self._cap))

    # size = property(lambda self: int(math.sqrt(self._cap)))

You can (micro)optimize this by precomputing the square root:

class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._size = int(math.sqrt(self._cap))

    @property
    def size(self):
        return self._size