pythonpython-decoratorsreadonly-attributewriteonly

Why does hasattr behave differently on classes and instances with @property method?


I implemented a write-only property in my class with @property. The weird thing is that hasattr behaves differently on the class and corresponding instance with this property.

from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin


class User(Base, UserMixin):
    # codes omitted...

    @property
    def password(self):
        raise AttributeError("password is a write-only attribute!")

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)
In [6]: hasattr(User,'password')
Out[6]: True

In [7]: u1=User()

In [9]: hasattr(u1,'password')
Out[9]: False

In [12]: getattr(User,'password')
Out[12]: <property at 0x1118a84a8>

In [13]: getattr(u1,'password')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-13-b1bb8901adc7> in <module>
----> 1 getattr(u1,'password')

~/workspace/python/flask_web_development/fisher/app/models.py in password(self)
     82     @property
     83     def password(self):
---> 84         raise AttributeError("password is a write-only attribute!")
     85
     86     @password.setter

AttributeError: password is a write-only attribute!

From the result of getattr, getattr(u1, 'password') tries to execute the method and raises an error, while getattr(User, 'password') doesn't execute the @property method. Why do they behave differently?


Solution

  • Properties are descriptors.


    Regarding getattr:

    When you access an attribute via getattr or the dot-notation on an object (u1) and the class of that object (User) happens to have a descriptor going by the name you are trying to access, that descriptor's __get__ method is called1, as happens when you issue getattr(u1, 'password'). In your specific case, the logic you defined in your getter (raising the AttributeError) will be executed.

    With getattr(User, 'password') the instance passed to the __get__ method is None, in which case __get__ just returns the descriptor itself instead of executing the getter logic you implemented.

    1There are some special rules depending on whether you have a data or a non-data descriptor, as explained in the Descriptor HowTo.


    Regarding hasattr:

    hasattr(u1, 'password') returns False because getattr(u1, 'password') raises an error. See this question.