Thanks to visit codestin.com
Credit goes to www.pythonmorsels.com

Python's __setattr__ method PREMIUM

Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read Python 3.10—3.14
Tags

A property can control what happens when a single attribute is assigned on your object. But what if you want to capture all attribute assignments?

The dunder method for attribute assignments

Python evaluates attribute assignments by calling an object's __setattr__ method. That __setattr__ method is the dunder method that Python uses for attribute assignments.

So this:

>>> some_object.some_attribute = 4

Is essentially the same as this:

>>> some_object.__setattr__(some_attribute, 4)

The actual attribute assignment is handled by the default __setattr__ implementation (implemented on the object class).

>>> class Thing: pass
...
>>> Thing.__setattr__
<slot wrapper '__setattr__' of 'object' objects>

Customizing attribute assignments

To customize attribute assignments on our own objects, we could implement a __setattr__ method on our class. Here's a class that implements case-insensitive attribute lookups and assignments, by lowercasing all attributes during assignment and during lookups (thanks to __getattr__):

class LowerAttributes:
    def __getattr__(self, name):
        return getattr(self, name.casefold())
    def __setattr__(self, name, value):
        return super().__setattr__(name.casefold(), value)

Here we're inheriting from this class to make a new Point class and then creating a new Point object:

>>> class Point(LowerAttributes):
...     def __init__(self, x, y, z):
...         self.x, self.y, self.z = x, y, z
...
>>> p = Point(1, 2, 3)

Now we can lookup or assign attributes using their uppercase or lowercase form and both will work interchangeably:

>>> p.X
1
>>> p.x
1
>>> p.Y = 5
>>> p.y
5

If it seems odds that we're calling super in a class that doesn't explicitly inherit from another class, note that all classes inherit from object, so while this is an odd-looking but sensible use of super().

Creating immutable objects

The most common uses of __setattr__ is to make immutable objects.

Here's a class that implements a __setattr__ method which always raises an AttributeError:

class Immutable:
    def __setattr__(self, name, value):
        raise AttributeError("Cannot assign attributes (object is immutable)")

Whenever any attribute assignments are attempted on an instance of this Immutable class, an exception will be raised:

>>> i = Immutable()
>>> i.x = 4
Traceback (most recent call last):
  File "<python-input-11>", line 1, in <module>
    i.x = 4
    ^^^
  File "<python-input-7>", line 3, in __setattr__
    raise AttributeError("Cannot assign attributes (object is immutable)")
AttributeError: Cannot assign attributes (object is immutable)

Unfortunately, this means that all attribute assignments will raise an exception... even in a __init__ method!

>>> class Point(Immutable):
...     def __init__(self, x, y, z):
...         self.x, self.y, self.z = x, y, z
...
>>> p = Point(1, 2, 3)
Traceback (most recent call last):
  File "<python-input-9>", line 1, in <module>
    p = Point(1, 2, 3)
  File "<python-input-8>", line 3, in __init__
    self.x, self.y, self.z = x, y, z
    ^^^^^^
  File "<python-input-7>", line 3, in __setattr__
    raise AttributeError("Cannot assign attributes (object is immutable)")
AttributeError: Cannot assign attributes (object is immutable)

Python sees no distinction between code executed in __init__ and code executed anywhere else An attribute assignment is an attribute assignment regardless of where it's performed.

To workaround this, we could manually call object.__setattr__ in our class's initializer method:

>>> class Point(Immutable):
...     def __init__(self, x, y, z):
...         object.__setattr__(self, "x", x)
...         object.__setattr__(self, "y", y)
...         object.__setattr__(self, "z", z)
...

Now we can create a new Point object without an error:

>>> p = Point(1, 2, 3)
>>> p.x
1

But typical attribute assignments will still raise exceptions:

>>> p.x = 4
Traceback (most recent call last):
  File "<python-input-15>", line 1, in <module>
    p.x = 4
    ^^^
  File "<python-input-7>", line 3, in __setattr__
    raise AttributeError("Cannot assign attributes (object is immutable)")
AttributeError: Cannot assign attributes (object is immutable)

This might seem like an odd hack, but it's the usual way to assign attributes on frozen dataclasses and other "immutable" objects. The Python documentation for object.__setattr__ also notes this approach for invoking Python's the low-level assignment setting mechanism.

Be mindful of recursion

Note that when implementing __setattr__, you'll need to make sure not to call your own __setattr__ method.

This would cause infinite recursion:

class LowerAttributes:
    def __getattr__(self, name):
        return getattr(self, name.casefold())
    def __setattr__(self, name, value):
        return setattr(self, name.casefold(), value)

Note the usage of the built-in setattr function in our __setattr__ method above. Using setattr will call self.__setattr__, which will call the exact method that we're currently defining!

To fix the problem, we'll need to call object.__setattr__ directly:

class LowerAttributes:
    def __getattr__(self, name):
        return getattr(self, name.casefold())
    def __setattr__(self, name, value):
        return object.__setattr__(self, name.casefold(), value)

Or use super().__setattr__ to delegate to our parent class's __setattr__ (which will very likely call object.__setattr__):

class LowerAttributes:
    def __getattr__(self, name):
        return getattr(self, name.casefold())
    def __setattr__(self, name, value):
        return super().__setattr__(name.casefold(), value)

Use __setattr__ to customize attribute assignments

If you need to customize what happens when you assign an attribute, Python's __setattr__ method is the way to do it.

If you also need to customize attribute lookups look into __getattr__ and __getattribute__. If you need to customize attribute deletion, look into __delattr__.

See the attribute access section of the Every dunder method in Python article for more.

This is a free preview of a premium article. You have 2 previews remaining.