The Attribute Lookup Protocol
The Attribute Lookup Protocol
Every obj.attr access follows one order — knowing it explains most attribute mysteries:
- Data descriptor on the type (defines
__get__and__set__) - Instance
__dict__entry forattr - Non-data descriptor or class attribute (only
__get__, or a plain value) AttributeError
class D: # data descriptor — wins over instance dict
def __get__(self, obj, owner=None): return 0
def __set__(self, obj, value): pass
class ND: # non-data descriptor — instance dict wins
def __get__(self, obj, owner=None): return 0
class C:
d, nd = D(), ND()
Methods are non-data descriptors bound at lookup time; property, classmethod, and staticmethod also participate. super().attr and obj.__dict__ peek at the layers underneath.
Building Reusable Descriptors
Building Reusable Descriptors
class Validated:
def __init__(self, kind):
self.kind = kind
def __set_name__(self, owner, name): # 3.6+: receives the attribute name
self.name = name
def __get__(self, obj, owner=None):
if obj is None:
return self # accessed on the class
return obj.__dict__.get(self.name, self.kind())
def __set__(self, obj, value):
if not isinstance(value, self.kind):
raise TypeError(f"{self.name} expects {self.kind.__name__}")
obj.__dict__[self.name] = value # delegate storage to the instance
class Money:
cents = Validated(int)
m = Money(); m.cents = 100
One descriptor validates fields across many classes — the pattern behind ORM fields (django.db.models.CharField). __set_name__ avoids passing the name twice.
Properties, Pitfalls, and Interviews
Properties, Pitfalls, and Interviews
property is just the stdlib data descriptor for getter/setter/deleter triples:
class Temp:
def __init__(self, c): self._c = c
@property
def f(self): return self._c * 9 / 5 + 32
Pitfalls:
- Non-data descriptors are shadowable. If your class defines only
__get__, an instance attribute of the same name silently overrides it — usually a bug, occasionally a feature (caching a bound method) - Missing
__set__flips the descriptor class; store values where you intend propertyon the class, not the instance:t.f.getter(...)fails —t.falready invoked__get__- slots are type-created descriptors; unknown attributes raise
AttributeError
Interview angle: recite the 4-step lookup order, then explain how caching stores into obj.__dict__ while the descriptor keeps first lookup.
Quiz
1. What makes a descriptor a "data" descriptor?
2. Why does instance.__dict__ sometimes win over a class descriptor?
3. What does __set_name__ provide?
Flashcards
Question
Data vs non-data descriptor
Click to reveal answer
Answer
Data = defines __set__ (beats instance dict); non-data = __get__ only (loses to instance dict).
Question
property
Click to reveal answer
Answer
Stdlib data descriptor wrapping getter/setter/deleter functions into one attribute.
Question
__set_name__
Click to reveal answer
Answer
Hook receiving the attribute name at class-creation time for self-registering fields.
Question
Attribute lookup order
Click to reveal answer
Answer
type data descriptor → instance __dict__ → non-data descriptor/class attr → AttributeError.
Revision Notes
Key Takeaways
- 1. Attribute access is a 4-step protocol; most "why is my value ignored" bugs are ordering issues
- 2. Descriptors package reusable validation/storage logic at the type level
- 3. property covers one-class cases; hand-written descriptors cover patterns repeated across classes
Interview Tips
- • State the lookup order precisely and name one consequence of each position
- • Use the ORM-field example to connect descriptors to real frameworks interviewers know
Cheat Sheet
lookup: data descriptor > instance dict > non-data descriptor; get/set/delete + set_name; property = built-in data descriptor.