__getattr__ Called When Paired with __setattr__

Hi,

I am testing the following test script. As you know, the __getattr__ method gets called when fetching previously undefined attributes. However, if the __setattr__ is added (uncommented), the __getattr__ method is still called even for existing attributes.

Can someone please help in making sense of this.

Here is the test script:

class Catcher:

    def __init__(self):

        self.name = 100
        self.other = 500
        self.planet = 'earth'
        print('\nDone instantiation.\n')

    def __getattr__(self, name):  # Called if fetching attribute
        print(f"Get: {name}")     # that does not yet exist

    # Comment / uncomment method for testing purposes
    def __setattr__(self, name, value):  
        print(f'Set: {name} {value}')

X = Catcher()

X.job      # Prints "Get: job"
X.job = 'carpenter'
X.pay      # Prints "Get: pay"
X.pay = 99 # Prints "Set: pay 99"
print('\nThese two attrs already exist:')
X.job      # '__getattr__' should not be called since it now exists
X.pay      # '__getattr__' should not be called since it now exists
X.name

No, it isn’t.

The problem in your code is that the __setattr__ method has to actually set attributes in order for them to get set. __setattr__ is not a “hook” for additional work to do when an attribute is set. It is a replacement for the attribute-setting logic. So if you want the attribute to actually get set, you need to do it yourself - for example, by explicitly calling the object.__setattr__ logic.

It’s like this so that you can set a different attribute name internally, or conditionally reject an attempt to set the attribute, etc.

1 Like

I get a bit confused in your description, it’s not clear to me that all of your g-s and s-s are straight :slight_smile:

To hopefully answer the question anyway, the __getattr__ method is only called if the attribute is not found in the object’s __dict__ when an attempt is made to access it, but __setattr__ is always called when an attempt is made to set an attribute. Note that your __setattr__ implementation is not actually doing anything, so the attribute is not set and __getattr__ will still be called because X.job and X.pay still don’t exist.

The fix is to have the __setattr__ method call __setattr__ on the superclass:

    def __setattr__(self, name, value):
        object.__setattr__(self, name, value)
        print(f'Set: {name} {value}')
1 Like

Thank you @MRAB!

Yes, this is the ticket. However, what do you mean by ‘on the superclass’ if I don’t have a superclass that I am inheriting? Can you please elaborate further so that it may be better understood.

There will always be a superclass. If, as in your code, you don’t specify one explicitly, it’ll default to object, as I used in the code I posted.

1 Like

Yup, I got them straightened out. Sometimes there is a misfire with the neurons upstairs. :wink:

I actually performed introspection, and it shows that they are part of instance attributes. For example, if I comment out the __setattr__ method, and perform the following:

print('\n--- Starting introspection.\n')
for key in X.__dict__:         
    print(f"{key:<6} {getattr(X, key):>2}") # key-value pair

print('\n--- End introspection.')

We see that they are part of the __dict__ and attributes of the instance.

Thank you @kknechtel for responding to my query.

If it isn’t, why do I get the following output:

Get: job
Get: pay
Get: name

The Get: string is from the __getattr__ method.

Yes, @MRAB set it straight in his reply.

Oh, ok. I believe there is theory up ahead regarding this concept. I appreciate your insight.
All of the feedback collectively helps me understand this a bit better.

Again, thank you.

Because it isn’t being called for existing attributes. It’s being called for non existing attributes. :wink:

Yes, I have been performing additional testing. The __setattr__ method as it was currently set-up, wasn’t actually doing any assignments (if it is in the code, it overrides the current default attribute-setting logic as @kknechtel had correctly pointed out). Via the output print statements, as they were set up, were giving the false impression that assignments were taking place when they actually weren’t.

When I stated that introspection showed the attributes listed in the __dict__, it was because I was commenting out the __setattr__ method thereby allowing Python’s default attribute-assigning logic to be applied. Again, my understanding at that time was incorrect.

I now fully understand all of your feedback.

Thank you all a bunch. :slightly_smiling_face: