In this chapter, we looked at several ways to work with an object's attributes. We can use the built-in features of the object class to get and set attribute values simply and effectively. We can use @property to create attribute-like methods.
If we want more sophistication, we can tweak the underlying special method implementations for __getattr__(), __setattr__(), __delattr__(), or __getattribute__(). These allow us very fine-grained control over attribute behaviors. We walk a fine line when we touch these methods because we can make fundamental (and confusing) changes to Python's behavior.
Internally, Python uses descriptors to implement features such as class methods, static methods, and properties. Many of the really good use cases for descriptors are already first-class features of the language.
The use of type hints helps confirm that objects are used...