To make a read-only data descriptor, define both$_{__get__()}$ and$_{__set__()}$ with the$_{__set__()}$ raising an AttributeError when called. Defining the __set__() method with an exception raising placeholder is enough to make it a data descriptor. 

## 3.4 Overview of descriptor invocation 

A descriptor can be called directly with$_{desc.__get__(obj)}$ or$_{desc.__get__(None, cls)}$ . 

But it is more common for a descriptor to be invoked automatically from attribute access. 

The expression obj.x looks up the attribute x in the chain of namespaces for obj . If the search finds a descriptor outside of the instance __dict__ , its __get__() method is invoked according to the precedence rules listed below. 

The details of invocation depend on whether$_{obj}$is an object, class, or instance of super. 

## 3.5 Invocation from an instance 

Instance lookup scans through a chain of namespaces giving data descriptors the highest priority, followed by instance variables, then non-data descriptors, then class variables, and lastly$_{__getattr__()}$ if it is provided. 

If a descriptor is found for$_{a.x}$ , then it is invoked with:$_{desc.__get__(a, type(a))}$ . 

The logic for a dotted lookup is in$_{object.__getattribute__()}$ . Here is a pure Python equivalent: 

def object_getattribute (obj, name): "Emulate PyObject_GenericGetAttr() in Objects/object.c " null=object() objtype =type(obj) cls_var=getattr(objtype, name, null) descr_get=getattr(type(cls_var), '__get__ ', null) if descr_get is not null: if (hasattr(type(cls_var), '__set__ ') or hasattr(type(cls_var), '__delete__ ')): return descr_get(cls_var, obj, objtype) # data descriptor if hasattr(obj,'__dict__ ') and name in vars(obj): return vars(obj)[name] # instance variable if descr_get is not null: return descr_get(cls_var, obj, objtype) # non-data descriptor if cls_var is not null: return cls_var # class variable raise AttributeError(name) 

Interestingly, attribute lookup doesn’t call$_{object.__getattribute__()}$ directly. Instead, both the dot op- erator and the$_{getattr()}$ function perform attribute lookup by way of a helper function: 

def getattr_hook(obj, name): "Emulate slot_tp_getattr_hook() in Objects/typeobject.c " try : return obj.__getattribute__ (name) except AttributeError: if not hasattr(type(obj), '__getattr__ '): raise return type(obj) .__getattr__ (obj, name) # __getattr__ 

So if$_{__getattr__()}$ exists, it is called whenever$_{__getattribute__()}$ raises$_{AttributeError}$ (either directly or in one of the descriptor calls). 

Also, if a user calls$_{object.__getattribute__()}$ directly, the$_{__getattr__()}$ hook is bypassed entirely. 

