LOAD_ATTR bytecode, __getattribute__ dispatch, the descriptor protocol, and type version tag caching.
Attribute lookup is the runtime process used to evaluate expressions such as:
obj.nameIt is one of the most important mechanisms in CPython. Method calls, properties, descriptors, inheritance, modules, classes, slots, special methods, and many object protocols depend on it.
A simple attribute expression can trigger a large amount of machinery:
find the object's type
search the type and its base classes
handle descriptors
check the instance dictionary
call custom attribute hooks
return a value or raise AttributeErrorAttribute lookup is dynamic. The result can depend on the runtime object, its class, its base classes, its instance dictionary, descriptors, metaclasses, and user-defined hooks.
33.1 Basic Attribute Access
The expression:
obj.xasks CPython to look up the attribute named "x" on obj.
If found, the lookup returns a Python object.
If missing, it raises AttributeError.
Example:
class C:
pass
obj = C()
obj.x = 10
print(obj.x)The assignment stores x in the instance dictionary:
obj.__dict__["x"] = 10The access finds it there.
33.2 Attribute Lookup Is Not Dictionary Lookup Only
For simple objects, attribute access may look like dictionary lookup:
obj.__dict__["x"]But full attribute lookup is more complex.
This expression:
obj.xmay involve:
data descriptors
instance dictionary
non-data descriptors
class attributes
base classes
__getattribute__
__getattr__
slots
properties
metaclass behaviorSo this equivalence is incomplete:
obj.x == obj.__dict__["x"]It only holds in simple cases.
33.3 Instance Dictionaries
Most normal Python objects have an instance dictionary.
class User:
pass
u = User()
u.name = "Ada"
u.age = 37
print(u.__dict__)Output:
{'name': 'Ada', 'age': 37}Instance attributes are stored in this dictionary unless the class uses slots or a custom layout.
For:
u.nameCPython can find "name" in u.__dict__.
But the instance dictionary is not always checked first. Data descriptors on the class have priority.
33.4 Class Attributes
Attributes can live on the class.
class User:
kind = "human"
u = User()
print(u.kind)Here, kind is not in u.__dict__. It is in User.__dict__.
Conceptually:
u.__dict__ does not contain "kind"
User.__dict__ contains "kind"
return "human"Class attributes are shared through the class:
a = User()
b = User()
print(a.kind)
print(b.kind)Both instances see the same class attribute unless an instance shadows it.
33.5 Instance Attribute Shadowing
An instance attribute can shadow a class attribute when the class attribute is not a data descriptor.
class User:
kind = "human"
u = User()
u.kind = "admin"
print(u.kind)
print(User.kind)Output:
admin
humanNow:
u.__dict__["kind"] = "admin"
User.__dict__["kind"] = "human"The instance attribute wins for u.kind.
This is why class attributes should be used carefully for mutable values:
class Bag:
items = []
a = Bag()
b = Bag()
a.items.append("x")
print(b.items)Both instances see the same list unless items is overridden on the instance.
33.6 Attribute Lookup Order
For normal instance attribute access, the lookup order is roughly:
1. Call type(obj).__getattribute__(obj, name)
2. Search the class and base classes for name
3. If a data descriptor is found, call descriptor.__get__
4. Otherwise, check obj.__dict__
5. If found in obj.__dict__, return that value
6. Otherwise, if a non-data descriptor was found, call descriptor.__get__
7. Otherwise, if a class attribute was found, return it
8. Otherwise, call __getattr__ if defined
9. Otherwise, raise AttributeErrorThe most important priority rule is:
data descriptor
before instance dictionary
instance dictionary
before non-data descriptor
non-data descriptor or class attribute
after instance dictionaryThis order explains properties, methods, slots, and shadowing.
33.7 Descriptors
A descriptor is an object that controls attribute access through one or more special methods:
__get__
__set__
__delete__Descriptors are stored on classes.
Example:
class Descriptor:
def __get__(self, obj, cls):
return "computed"
class C:
x = Descriptor()
obj = C()
print(obj.x)Accessing obj.x calls:
C.__dict__["x"].__get__(obj, C)Descriptors allow classes to customize what attribute access means.
They power:
methods
staticmethod
classmethod
property
slots
many built-in attributes
ORM fields
validation systems
lazy attributes33.8 Data Descriptors
A data descriptor defines __set__ or __delete__, usually with __get__.
class DataDescriptor:
def __get__(self, obj, cls):
return "from descriptor"
def __set__(self, obj, value):
print("set", value)
class C:
x = DataDescriptor()
obj = C()
obj.__dict__["x"] = "from dict"
print(obj.x)Output:
from descriptorThe descriptor wins over the instance dictionary because it is a data descriptor.
This is how property works.
class C:
@property
def x(self):
return 10
obj = C()
print(obj.x)The property object is a data descriptor.
33.9 Non-Data Descriptors
A non-data descriptor defines __get__ but not __set__ or __delete__.
Normal Python functions stored on classes are non-data descriptors.
class C:
def f(self):
return "method"
obj = C()
print(obj.f())The function descriptor binds obj as self.
But because it is non-data, an instance attribute can shadow it:
obj.f = lambda: "instance function"
print(obj.f())Now the instance dictionary wins.
This is why methods can be replaced per instance.
33.10 Properties
A property is a descriptor that calls functions during attribute access.
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius * self.radius
c = Circle(10)
print(c.area)The expression:
c.areacalls the property getter.
There is no explicit () in the source because the call is hidden inside descriptor access.
A setter adds assignment behavior:
class User:
def __init__(self):
self._name = ""
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value.strip()Assignment:
u.name = " Ada "calls the property setter.
33.11 Methods as Descriptors
Functions stored on classes are descriptors.
class C:
def f(self):
return 1
obj = C()
m = obj.fThe lookup:
obj.fcalls the function object’s descriptor logic and produces a bound method.
Conceptually:
C.__dict__["f"].__get__(obj, C)
-> bound methodThe bound method stores:
function = C.f
self = objThen:
obj.f()calls the function with self automatically inserted.
33.12 staticmethod and classmethod
staticmethod and classmethod are descriptor wrappers.
A static method disables binding:
class Math:
@staticmethod
def add(a, b):
return a + b
Math.add(1, 2)
Math().add(1, 2)No self or cls is inserted.
A class method binds the class:
class User:
@classmethod
def make(cls):
return cls()Calling:
User.make()passes User as the first argument.
Calling through an instance also passes the class:
User().make()33.13 Slots
__slots__ changes instance attribute storage.
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = yInstances of Point usually do not have a normal __dict__ unless explicitly requested.
p = Point(1, 2)
print(p.x)Slot attributes are implemented with descriptors stored on the class.
Conceptually:
Point.__dict__["x"] = slot descriptor
Point.__dict__["y"] = slot descriptorAccessing p.x invokes the slot descriptor, which reads from a fixed location in the object layout.
Slots can reduce memory use and speed up some attribute access patterns, but they also limit dynamic instance attributes.
33.14 Missing Attributes
If normal lookup fails, Python may call __getattr__.
class Dynamic:
def __getattr__(self, name):
if name == "answer":
return 42
raise AttributeError(name)
d = Dynamic()
print(d.answer)__getattr__ is called only after the normal lookup path fails.
This is useful for:
lazy loading
proxy objects
RPC clients
compatibility layers
dynamic APIs
mock objectsIf __getattr__ also fails, it should raise AttributeError.
33.15 __getattribute__
__getattribute__ intercepts all normal attribute access.
class Traced:
def __getattribute__(self, name):
print("lookup", name)
return super().__getattribute__(name)
def f(self):
return 1
obj = Traced()
obj.f()Every access goes through __getattribute__, including method lookup.
A custom implementation must avoid infinite recursion:
class Bad:
def __getattribute__(self, name):
return self.__dict__[name]Accessing self.__dict__ calls __getattribute__ again.
Use object.__getattribute__ or super().__getattribute__:
class Good:
def __getattribute__(self, name):
return object.__getattribute__(self, name)33.16 Attribute Assignment
Assignment:
obj.x = valueuses attribute setting logic.
The rough order is:
1. Call type(obj).__setattr__(obj, name, value)
2. If a data descriptor with __set__ exists, use it
3. Otherwise, store into obj.__dict__ if available
4. Otherwise, use slot storage if applicable
5. Otherwise, raise AttributeErrorA custom __setattr__ can intercept assignment:
class C:
def __setattr__(self, name, value):
print("set", name, value)
super().__setattr__(name, value)
obj = C()
obj.x = 10As with __getattribute__, careless code can recurse.
33.17 Attribute Deletion
Deletion:
del obj.xuses deletion logic.
It may call:
type(obj).__delattr__
descriptor.__delete__
remove from instance dictionary
clear slot valueExample descriptor:
class D:
def __delete__(self, obj):
print("delete")
class C:
x = D()
obj = C()
del obj.xDeletion is part of the same descriptor and attribute protocol family.
33.18 Class Attribute Lookup
Classes are objects too.
class C:
x = 10
print(C.x)Looking up C.x is attribute access on a class object.
The type of C is usually type, so class attribute lookup is controlled by metaclass behavior.
Conceptually:
object = C
type(object) = type
lookup attribute "x"This is why metaclasses can customize class-level attribute access.
33.19 Metaclass Attribute Lookup
A metaclass can define attributes visible on classes.
class Meta(type):
def meta_method(cls):
return "meta"
class C(metaclass=Meta):
pass
print(C.meta_method())The method is found on the metaclass and bound to the class object.
Class attribute lookup involves:
attributes on the class itself
descriptors in the metaclass
metaclass MROMetaclass lookup is separate from instance lookup, but it uses the same general object model.
33.20 Module Attribute Lookup
Modules have attribute dictionaries.
import math
print(math.pi)This is roughly:
math.__dict__["pi"]Modules can also define __getattr__ for missing attributes.
# module.py
def __getattr__(name):
if name == "lazy":
return load_lazy()
raise AttributeError(name)Then:
module.lazycan be computed dynamically.
This feature is often used for lazy imports and compatibility shims.
33.21 Attribute Lookup and Inheritance
For instances, class lookup follows the method resolution order.
class A:
x = "A"
class B(A):
pass
obj = B()
print(obj.x)Lookup searches:
B
A
objectThe order is stored in:
print(B.__mro__)Multiple inheritance uses C3 linearization to compute a consistent MRO.
class A: pass
class B: pass
class C(A, B): pass
print(C.__mro__)Attribute lookup relies on this order.
33.22 Attribute Lookup and super
super() changes where lookup begins.
class Base:
def f(self):
return "base"
class Child(Base):
def f(self):
return super().f()Inside Child.f, super().f searches after Child in the MRO, then binds the found method to the original instance.
Conceptually:
MRO: Child, Base, object
super from Child
search Base, then object
bind result to selfsuper() is not simple parent-class access. It is MRO-relative descriptor lookup.
33.23 Special Method Lookup
Special methods are often looked up on the type, not through normal instance attribute lookup.
Example:
len(obj)uses the type’s length slot. It does not simply perform:
obj.__len__()This distinction matters:
class C:
def __len__(self):
return 10
obj = C()
obj.__len__ = lambda: 20
print(obj.__len__())
print(len(obj))The explicit call may find the instance attribute. The len() operation uses special method lookup through the type.
Special method lookup is designed for speed and consistency of the object protocol.
33.24 Attribute Lookup in Bytecode
Attribute access compiles to bytecode instructions such as:
LOAD_ATTR
STORE_ATTR
DELETE_ATTR
LOAD_METHODFor:
value = obj.xconceptual bytecode:
LOAD_FAST obj
LOAD_ATTR x
STORE_FAST valueFor:
obj.x = valueconceptual bytecode:
LOAD_FAST value
LOAD_FAST obj
STORE_ATTR xFor method calls:
obj.f(arg)CPython may use a method-specific load instruction to optimize binding and calling.
33.25 Attribute Lookup Can Execute Code
Attribute lookup is not always passive.
This expression:
obj.xcan call user code through:
__getattribute__
descriptor __get__
property getter
__getattr__
metaclass hooks
module __getattr__Therefore attribute access can:
raise exceptions
mutate state
perform I/O
allocate objects
return different values each time
call arbitrary Python codeExample:
class C:
@property
def x(self):
print("computed")
return 10
obj = C()
obj.x
obj.xThe getter runs each time.
33.26 Attribute Lookup and Exceptions
If an attribute is missing, Python raises AttributeError.
obj.missingBut attribute lookup can raise other exceptions too.
Example:
class C:
@property
def x(self):
raise RuntimeError("failed")
obj = C()
obj.xThis raises RuntimeError, not AttributeError.
Only missing attributes should generally raise AttributeError. Tools such as hasattr depend on this convention.
hasattr(obj, "x")works by attempting lookup and catching AttributeError.
33.27 Attribute Lookup and hasattr
hasattr(obj, name) calls attribute lookup.
hasattr(obj, "x")is roughly:
try:
getattr(obj, "x")
except AttributeError:
return False
else:
return TrueThis means hasattr can execute user code.
If a property getter raises RuntimeError, hasattr does not treat that as a missing attribute.
class C:
@property
def x(self):
raise RuntimeError("boom")
hasattr(C(), "x")The RuntimeError propagates.
33.28 Attribute Lookup and getattr
getattr performs dynamic attribute lookup.
getattr(obj, "name")is equivalent to:
obj.namewhen the attribute name is known statically.
It also supports a default:
getattr(obj, "missing", default)This returns default only if lookup raises AttributeError.
It does not suppress arbitrary exceptions from descriptors or custom lookup hooks.
33.29 Attribute Lookup and setattr
setattr performs dynamic attribute assignment.
setattr(obj, "x", 10)is equivalent to:
obj.x = 10for a static name.
It still respects:
__setattr__
data descriptors
slots
read-only attributesSo setattr is not a raw dictionary write.
33.30 Attribute Lookup and delattr
delattr performs dynamic attribute deletion.
delattr(obj, "x")is equivalent to:
del obj.xIt still respects:
__delattr__
descriptor __delete__
slot deletion
instance dictionary deletion33.31 Attribute Lookup and Inline Caches
Attribute lookup is frequent, so CPython optimizes it.
A repeated access:
for obj in objects:
total += obj.valuemay hit the same attribute layout many times.
CPython can attach inline cache data to the bytecode instruction. The cache may store facts such as:
expected receiver type
type version tag
dictionary version
descriptor result
slot offset
instance dictionary offsetOn later executions:
if guards still hold:
use fast path
else:
fall back to generic lookupThis preserves dynamic semantics while accelerating stable cases.
33.32 Cache Invalidation
Python allows class mutation.
class C:
x = 1
obj = C()
print(obj.x)
C.x = 2
print(obj.x)The second lookup must see 2.
Therefore an attribute cache cannot blindly reuse old results. It must guard against changes.
Typical guards may involve:
type identity
type version
dictionary version
descriptor kind
instance layoutWhen assumptions fail, CPython falls back to generic lookup and may update the cache.
33.33 Instance Layout Stability
Attribute caches work best when object layouts are stable.
Example:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
points = [Point(i, i + 1) for i in range(1000)]
for p in points:
p.xEach p has the same type and likely similar attribute layout. This is a good case for specialization.
Dynamic changes can reduce cache effectiveness:
p.z = 10
del p.x
Point.x = property(...)The interpreter must preserve correctness first.
33.34 Attribute Lookup and Dictionaries
Instance dictionaries are highly optimized.
CPython dictionaries are central to object namespaces:
module namespace
class namespace
instance namespace
globals
builtinsFor many objects, attribute lookup ultimately becomes dictionary lookup plus descriptor handling.
Attribute caches often depend on dictionary versioning so the interpreter can tell whether a namespace changed.
33.35 Attribute Lookup and Modules
Module globals are also dictionary-based.
import math
math.sqrtThe module object stores attributes in its dictionary.
Module lookup is simpler than instance descriptor lookup in many cases, but modules can customize missing attribute behavior through __getattr__.
Imports also bind module attributes:
import package.submoduleAfter import, the package may have a submodule attribute.
33.36 Attribute Lookup and Builtins
Built-in types often use specialized internal layouts and descriptors.
Example:
list.append
dict.get
str.upper
int.bit_lengthThese attributes are typically method descriptors implemented in C.
Access through an instance:
[].appendreturns a bound built-in method object.
Immediate calls may use optimized method-call paths.
33.37 Attribute Lookup and Memory Management
Attribute lookup manipulates references.
When an attribute is returned, CPython must ensure the result stays alive.
If lookup creates a bound method, property result, or descriptor result, reference ownership must be handled correctly.
Temporary objects may be created during lookup:
bound methods
property return values
descriptor return values
exception objects
strings or proxy objects in custom hooksFailure paths must clean them up.
Because attribute lookup can call Python code, reference safety is critical.
33.38 Attribute Lookup and Reentrancy
Attribute lookup can reenter Python.
Example:
class C:
def __getattribute__(self, name):
return compute_value(name)The call to compute_value may execute arbitrary Python code.
This means during one attribute lookup, Python code can:
mutate the object
mutate the class
change descriptors
trigger garbage collection
raise exceptions
call back into the same objectThe lookup implementation must tolerate this.
33.39 Attribute Lookup and Proxies
Proxy objects often implement custom attribute lookup.
class Proxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name):
return getattr(self._target, name)Then:
proxy.xdelegates to:
target.xA robust proxy must handle special methods carefully because implicit special method lookup often bypasses normal instance attribute lookup.
For example, implementing __getattr__ alone may not make len(proxy) delegate to len(target).
33.40 Attribute Lookup and ORMs
Many ORMs use descriptors.
Example shape:
class Field:
def __get__(self, obj, cls):
return obj._data[self.name]
def __set__(self, obj, value):
obj._data[self.name] = value
class User:
name = Field()Then:
user.namedoes not read a plain attribute. It calls Field.__get__.
This allows libraries to implement:
validation
lazy loading
database column mapping
change tracking
computed fields
relationship loadingDescriptors turn attribute syntax into programmable access.
33.41 Attribute Lookup and Dataclasses
Dataclasses do not fundamentally change attribute lookup.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: intInstances usually store fields in the instance dictionary unless slots are enabled.
p = Point(1, 2)
print(p.x)This is ordinary instance attribute lookup.
With slots:
@dataclass(slots=True)
class Point:
x: int
y: intfield storage uses slots rather than a normal instance dictionary.
33.42 Attribute Lookup and Type Objects
Types are objects, and they also have attributes.
int.bit_length
str.upper
dict.itemsThese are attributes on type objects.
When looking up attributes on instances, the instance’s type controls lookup.
When looking up attributes on classes, the metaclass controls lookup.
This recursive object model is central to Python:
object has type
type is also an object
type has metaclass
metaclass controls class attribute lookup33.43 Attribute Lookup and Performance
Attribute access has more overhead than local variable access.
Compare:
xinside a function, where x is local:
LOAD_FASTwith:
obj.xwhich requires:
LOAD_FAST obj
LOAD_ATTR xLOAD_ATTR may involve descriptor logic, dictionary lookup, cache checks, type version checks, and error handling.
This is why hot loops sometimes benefit from local binding:
append = xs.append
for item in items:
append(item)The benefit depends on Python version and specialization. Modern CPython optimizes method calls aggressively, so manual local binding is not always faster.
33.44 Attribute Lookup and Errors in Hot Paths
Attribute lookup failure is relatively expensive because it constructs an exception.
Example:
try:
value = obj.missing
except AttributeError:
value = defaultFor frequent misses, using a dictionary or explicit sentinel may be faster.
But the best design depends on semantics. Attribute lookup failure is correct and idiomatic when absence is exceptional or when interacting with dynamic APIs.
33.45 Inspecting Attribute Lookup
Use vars to inspect instance dictionaries:
class C:
pass
obj = C()
obj.x = 1
print(vars(obj))Use class dictionaries:
print(C.__dict__)Use MRO:
print(C.__mro__)Use inspect.getattr_static to inspect attributes without triggering normal dynamic lookup in many cases:
import inspect
inspect.getattr_static(obj, "x")This is useful when descriptors or __getattr__ would execute code.
33.46 A Minimal Attribute Lookup Model
A simplified lookup model:
def lookup(obj, name):
cls = type(obj)
class_attr = find_in_mro(cls, name)
if is_data_descriptor(class_attr):
return class_attr.__get__(obj, cls)
if hasattr(obj, "__dict__") and name in obj.__dict__:
return obj.__dict__[name]
if has_get(class_attr):
return class_attr.__get__(obj, cls)
if class_attr is not missing:
return class_attr
getattr_hook = find_in_mro(cls, "__getattr__")
if getattr_hook is not missing:
return getattr_hook(obj, name)
raise AttributeError(name)This omits important real details:
custom __getattribute__
metaclasses
slots internals
C-level fast paths
reference counts
inline caches
error handling
module lookup
special method lookupBut it captures the normal descriptor priority order.
33.47 Common Misunderstandings
| Misunderstanding | Correct model |
|---|---|
obj.x always reads obj.__dict__["x"] | Data descriptors, slots, class attributes, and hooks may intervene |
| Methods are stored on each instance | Normal methods are stored on the class and bound during lookup |
| Properties are fields | Properties are descriptors that call functions |
__getattr__ handles every lookup | It only runs after normal lookup fails |
__getattribute__ handles missing attributes only | It handles all normal attribute access |
| Instance attributes always beat class attributes | Data descriptors beat instance attributes |
| Special methods always use ordinary lookup | Many are looked up through type slots |
| Attribute access is side-effect free | It can execute arbitrary Python code |
33.48 Reading Strategy
To study attribute lookup, build examples in this order:
class C:
x = 1Then add:
obj.x = 2Then add a method:
def f(self): ...Then replace it with:
@property
def x(self): ...Then add:
__getattr__
__getattribute__
__slots__
staticmethod
classmethod
multiple inheritance
metaclassFor each step, inspect:
vars(obj)
C.__dict__
C.__mro__
type(obj)And disassemble access sites:
import dis
def read(obj):
return obj.x
dis.dis(read)This builds a practical map from syntax to object model behavior.
33.49 Chapter Summary
Attribute lookup is the mechanism behind obj.name. It is a dynamic protocol involving object type, class dictionaries, instance dictionaries, descriptors, inheritance, custom hooks, slots, modules, metaclasses, and inline caches.
The core lookup priority for normal instance attributes is:
data descriptor
instance dictionary
non-data descriptor
class attribute
__getattr__
AttributeErrorThis order explains methods, properties, shadowing, slots, and many framework patterns.
CPython optimizes attribute lookup heavily with specialized bytecode paths and inline caches, but it must preserve Python’s dynamic semantics. Classes can change, instances can change, descriptors can run code, and custom hooks can override the entire process.