Properties in Python is a nice syntactic sugar for accessing attributes of an object/class, actually functioning as the setters, getters, etc of typical OOP paradigm. Though, since properties are syntactic sugar, they actually are using uderlying mechanisms of Python. For example, every object in Python has the method .__setattr__, which is called to directly set the an attribute of the object, either creating the attribute or altering its value. 

Having said the above, this post is about the question: 

if in Python one uses:

@property
def a(self):
    return self._a

@a.setter
def a(self, new_val):
    self._a = a

then, can one override it using self.__setattr__ ? In other words, what is the order of execution for property setter and self.__setattr__ ?

To investigate the above, below is a snippet and its corresponding result

# -*- coding: utf-8 -*-

"""Snippet for checking the __setattr__ order of execution.
"""

__all__ = ["Foo"]
__docformat__ = "reStructuredText"
__author__ = "Konstantinos Drosos"

from pathlib import Path
import traceback


class Foo(object):
    def __init__(self) -> None:
        self._a = -1

    @property
    def a(self):
        return self._a

    @a.setter
    def a(self, n):
        print(f"  - Called setter `Foo.a` with value: {n}", flush=True)
        self._show_tracebak(traceback.extract_stack())
        self._a = n

    def __setattr__(self, name: str, value) -> None:
        print(
            f"  - Called `__setattr__` with name: {name} and value: {value}", flush=True
        )
        self._show_tracebak(traceback.extract_stack())
        return super().__setattr__(name, value)

    @staticmethod
    def _show_tracebak(trace_stack: traceback.StackSummary) -> None:
        print("    * Traceback:")
        for f_summary in trace_stack[:-1]:
            f_name = Path(f_summary.filename).name
            l_no = f_summary.lineno
            line = f_summary.line
            name = f_summary.name
            p = len("__setattr__") - len(name)
            print(f"      ** {f_name}, line {l_no}, {name}{' ' * p}: {line}")


def main():
    print("-" * 20)
    print("* Initializing object", flush=True)
    foo = Foo()

    print("-" * 20)
    print("* Will now use property setter foo.a", flush=True)
    foo.a = 10

    print("-" * 20)
    print("* Now altering property with `setattr`", flush=True)
    setattr(foo, "a", 20)

    print("-" * 20)
    print("* Now altering the _a property directly with `setattr`", flush=True)
    setattr(foo, "_a", 30)


if __name__ == "__main__":
    main()


which outputs:

$ python main.py
--------------------
* Initializing object
  - Called `__setattr__` with name: _a and value: -1
    * Traceback:
      ** test.py, line 69, module   : main()
      ** test.py, line 53, main       : foo = Foo()
      ** test.py, line 19, __init__   : self._a = -1
--------------------
* Will now use property setter foo.a
  - Called `__setattr__` with name: a and value: 10
    * Traceback:
      ** test.py, line 69, module   : main()
      ** test.py, line 57, main       : foo.a = 10
  - Called setter `Foo.a` with value: 10
    * Traceback:
      ** test.py, line 69, module   : main()
      ** test.py, line 57, main       : foo.a = 10
      ** test.py, line 34, __setattr__: return super().__setattr__(name, value)
  - Called `__setattr__` with name: _a and value: 10
    * Traceback:
      ** test.py, line 69, module   : main()
      ** test.py, line 57, main       : foo.a = 10
      ** test.py, line 34, __setattr__: return super().__setattr__(name, value)
      ** test.py, line 29, a          : self._a = n
--------------------
* Now altering property with `setattr`
  - Called `__setattr__` with name: a and value: 20
    * Traceback:
      ** test.py, line 69, module   : main()
      ** test.py, line 61, main       : setattr(foo, "a", 20)
  - Called setter `Foo.a` with value: 20
    * Traceback:
      ** test.py, line 69, module   : main()
      ** test.py, line 61, main       : setattr(foo, "a", 20)
      ** test.py, line 34, __setattr__: return super().__setattr__(name, value)
  - Called `__setattr__` with name: _a and value: 20
    * Traceback:
      ** test.py, line 69, module   : main()
      ** test.py, line 61, main       : setattr(foo, "a", 20)
      ** test.py, line 34, __setattr__: return super().__setattr__(name, value)
      ** test.py, line 29, a          : self._a = n
--------------------
* Now altering the _a property directly with `setattr`
  - Called `__setattr__` with name: _a and value: 30
    * Traceback:
      ** test.py, line 69, module : main() ** test.py, line 65, main : setattr(foo, "_a", 30)

As one can see, what happens when the foo.a=10 is called, is that:

  1. The __setattr__ method is called, and the execution is passed to the super() object. 
  2. The super() object is responsible to identify that a @property.setter exists, and calling it 
  3. Then, the @property.setter is actually calling the __setattr__ method of the object, in order to finally alter the value of the attribute.

Oddly enough, the above order is the exact one when the changing of the attribute is performed with the setattr(foo, "a", 20). This clearly shows that the @property.setter is indeed a syntactic sugar and ther base mechanism for alterning the properties in an object is the __setattr__ magic method. Additionally, the above show that the @property actually creates a method, which is a shorthand-call for the __setattr__. 

The above is further justified from the case setattr("foo", "_a", 20) (note the underscore at the "_a"). In this final case, the super() object does not call the @property.setter, since the attribute is not bind to a method.