Type version tags, LOAD_ATTR inline cache hits, and the attribute specialization guards for slots and descriptors.
Attribute access is one of the most frequent operations in Python. It appears whenever code reads, writes, or deletes a named field on an object.
obj.name
obj.name = value
del obj.nameAt source level, attribute access looks like a simple field operation. In CPython, it is a dynamic protocol involving object types, dictionaries, descriptors, slots, method binding, inheritance, inline caches, and specialization.
A simple expression:
obj.xmay involve:
checking the object's type
looking for descriptors on the class
looking in the instance dictionary
walking base classes
calling descriptor methods
calling __getattribute__
possibly calling __getattr__
raising AttributeError if no value existsAttribute access is therefore a major performance target.
78.1 Why Attribute Access Matters
Python code uses attributes constantly.
Common examples:
user.name
request.headers
response.status_code
path.name
module.function
self.value
cls.registryMany object-oriented programs are dominated by attribute and method access.
For example:
for item in items:
total += item.priceThe loop body performs attribute access on every iteration. If the loop runs one million times, the item.price bytecode site becomes a hot operation.
CPython optimizes this pattern by making repeated attribute access faster when the runtime shape is stable.
78.2 Attribute Access Is Not Field Access
In C, a struct field access can compile to a fixed memory offset:
p->xThis may become a direct load from memory.
In Python:
p.xdoes not mean “load from a fixed field.” It means “perform Python attribute lookup.”
That lookup must support:
instance attributes
class attributes
inherited attributes
properties
methods
slots
descriptors
custom __getattribute__
custom __getattr__
metaclassesThis flexibility is why attribute access has overhead.
78.3 The Default Lookup Path
For ordinary objects, attribute lookup begins with the type.
Conceptually, obj.x follows a path like this:
type(obj)
↓
look for data descriptor named x on type or bases
↓
look in obj.__dict__
↓
look for non-data descriptor or class attribute
↓
call descriptor if needed
↓
return value or raise AttributeErrorThis order is important.
Data descriptors take priority over instance dictionaries. Non-data descriptors are checked after the instance dictionary.
This rule explains properties and methods.
78.4 Data Descriptors
A data descriptor is an object with __set__ or __delete__, usually also __get__.
Example:
class C:
@property
def x(self):
return 42The property object is a descriptor.
For:
obj.xCPython does not simply look in obj.__dict__. It finds the property on the class and calls its descriptor logic.
Conceptually:
property.__get__(obj, type(obj))This makes properties powerful, but it means attribute access may execute arbitrary Python code.
78.5 Instance Dictionaries
Most normal Python objects store dynamic attributes in an instance dictionary.
Example:
class User:
pass
u = User()
u.name = "Alice"
u.age = 30The instance dictionary is roughly:
{
"name": "Alice",
"age": 30,
}Then:
u.nameusually becomes a dictionary lookup.
The dictionary key is the string "name".
This is flexible because attributes can be added at runtime:
u.email = "[email protected]"The cost is memory and lookup overhead.
78.6 Class Dictionaries
Classes also have dictionaries.
Example:
class C:
value = 10
def method(self):
return self.valueThe class dictionary contains:
"value" -> 10
"method" -> function objectWhen obj.method is accessed, CPython finds the function object in the class dictionary. Python functions implement the descriptor protocol, so the function binds itself to the instance and produces a method behavior.
Immediate method calls can avoid creating a temporary bound method object, but the lookup semantics remain descriptor-based.
78.7 Inheritance and the MRO
If an attribute is not found directly on the class, CPython searches base classes using the method resolution order.
Example:
class A:
x = 1
class B(A):
pass
b = B()
print(b.x)Lookup checks:
B
A
objectThe MRO is precomputed and stored on the type object.
Attribute access must respect this order exactly, including multiple inheritance and C3 linearization.
78.8 __getattribute__
Every attribute read calls the type’s attribute access machinery.
User classes inherit object.__getattribute__ by default.
A class can override it:
class C:
def __getattribute__(self, name):
print("reading", name)
return super().__getattribute__(name)Now every obj.x call goes through custom Python code.
This disables many simple fast-path assumptions. CPython must preserve the override semantics.
A custom __getattribute__ is powerful, but it makes every attribute read more expensive.
78.9 __getattr__
__getattr__ is different from __getattribute__.
It runs only when normal lookup fails.
Example:
class C:
def __getattr__(self, name):
return f"missing: {name}"Then:
C().xreturns:
missing: xThe normal lookup path still runs first. Only after it fails does CPython call __getattr__.
This preserves common attribute lookup behavior while allowing dynamic fallback.
78.10 Slots
__slots__ gives objects fixed attribute storage.
Example:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = yInstances of Point do not need a normal per-instance dictionary unless __dict__ is included in slots.
The object layout can contain fixed slots for x and y.
Conceptually:
Point instance:
slot 0 -> x
slot 1 -> yAttribute access can then become a checked offset load instead of a dictionary lookup.
Slots improve memory use and can improve attribute access speed, especially for many small objects with fixed fields.
78.11 Slot Descriptors
Slots are implemented through descriptors placed on the class.
For:
class Point:
__slots__ = ("x", "y")the class receives descriptor objects for x and y.
When CPython sees:
p.xthe slot descriptor knows the offset where the value is stored.
Specialized attribute access can cache that offset.
The fast path becomes:
check exact type
load slot value by offsetThis is one of the fastest attribute access forms in CPython.
78.12 Method Access
A method access:
obj.methodusually returns a bound method object.
The bound method stores:
underlying function
bound instanceExample:
m = obj.method
m()Here m must exist as a first-class object.
But for immediate calls:
obj.method()CPython can often avoid creating the bound method object. It can keep the function and instance separately and call the function with self inserted.
This optimization is critical because method calls are common.
78.13 Module Attributes
Module attributes are stored in the module dictionary.
Example:
import math
math.sqrt(9)The math.sqrt lookup is an attribute access on the module object.
Conceptually:
module.__dict__["sqrt"]Module attribute access is common in code that calls imported functions.
CPython can specialize module attribute loads when the module dictionary remains stable.
78.14 Attribute Writes
Attribute writes use related but separate machinery.
obj.x = valueThe assignment may:
call a data descriptor's __set__
write into a slot
write into the instance dictionary
fail if the object is read-only
fail if slots reject the nameFor ordinary objects, the write updates obj.__dict__.
For slot objects, the write stores into a fixed slot.
For properties with setters, the write calls descriptor code.
78.15 Attribute Deletion
Deletion also follows object protocol rules.
del obj.xThis may:
call a descriptor's __delete__
delete from the instance dictionary
clear a slot
raise AttributeErrorDeletion can invalidate inline caches because it mutates object or class state.
78.16 Type Version Tags
CPython uses type version tags to validate attribute caches.
If a class dictionary or relevant type state changes, the type version changes.
An inline cache can store:
expected type pointer
expected type version
cached lookup result or offsetFast path:
if type(obj) == cached_type
and cached_type version is unchanged:
use cached lookup
else:
fall back to generic lookupThis makes repeated attribute access cheap while preserving dynamic behavior.
78.17 Dictionary Version Tags
Instance dictionaries and module dictionaries also have version tags.
A global or module attribute cache may store:
expected dictionary version
cached valueWhen the dictionary mutates, the version changes.
This invalidates the cached assumption without scanning the dictionary.
Version tags are the foundation of many fast attribute and global lookup paths.
78.18 Inline Caches for LOAD_ATTR
The bytecode instruction for reading an attribute is commonly represented as LOAD_ATTR.
For:
obj.xthe interpreter executes a LOAD_ATTR-family instruction.
Modern CPython can specialize this instruction based on observed runtime behavior.
Possible specialized paths include:
instance dictionary value
slot value
module attribute
class attribute
method load
descriptor pathThe cache is stored near the instruction, so the lookup site has local remembered state.
78.19 Cache Hit Path
A fast attribute cache hit is deliberately small.
Example for a slot-like access:
load obj
check Py_TYPE(obj) == cached_type
check type version unchanged
read value from cached offset
push resultThis avoids:
hashing attribute name
searching dictionaries
walking MRO
running generic descriptor logicThe validation still protects semantics.
78.20 Cache Miss Path
A cache miss falls back to generic lookup.
Misses happen when:
object type differs
class dictionary changed
instance dictionary layout changed
attribute was deleted
descriptor changed
custom lookup is involvedThe miss path may also update the cache or cause deoptimization.
The key invariant:
cache miss changes performance, not correctness78.21 Attribute Access and Specialization
The adaptive interpreter observes each attribute access site.
Example:
def get_x(obj):
return obj.xAt first, the LOAD_ATTR is generic or adaptive.
After repeated calls with the same shape:
get_x(Point(1, 2))
get_x(Point(3, 4))
get_x(Point(5, 6))CPython may specialize the instruction.
The specialization belongs to the bytecode site, not to the attribute name globally.
Another function reading .x may specialize differently.
78.22 Monomorphic Attribute Sites
A monomorphic attribute site sees one dominant type.
Example:
for p in points:
total += p.xIf every p is a Point, this site is monomorphic.
Monomorphic sites specialize well.
The cache only needs to remember one type and one lookup path.
78.23 Polymorphic Attribute Sites
A polymorphic site sees multiple types.
Example:
def get_name(obj):
return obj.namecalled with:
User
Team
Project
OrganizationA simple inline cache may miss frequently.
CPython can still optimize some cases, but very unstable sites tend to remain generic.
The performance cost comes from repeated failed assumptions.
78.24 Megamorphic Sites
A megamorphic site sees many unrelated shapes.
Example:
for obj in mixed_objects:
handle(obj.value)where mixed_objects contains dozens of unrelated types.
In this case, specialization provides limited value.
The interpreter may back off to avoid repeated specialization attempts.
Megamorphic sites are common in plugin systems, serialization frameworks, ORMs, and dynamic dispatch-heavy code.
78.25 Attribute Access and __dict__
Objects with normal instance dictionaries are flexible.
obj.any_new_name = valueThis flexibility has costs:
per-instance dictionary memory
hash table lookup
dictionary version management
cache invalidationFor many objects with the same fields, split dictionaries reduce memory by sharing keys.
For fixed-layout objects, slots can reduce memory further.
78.26 Split Instance Dictionaries
For many user-defined classes, CPython uses split dictionaries.
Instances of the same class can share a key table.
Example:
class User:
def __init__(self, name, age):
self.name = name
self.age = ageMany instances share keys:
name
ageEach instance stores only values:
user1 values: ["Alice", 30]
user2 values: ["Bob", 40]This improves memory use and can help attribute access locality.
78.27 Attribute Access and Globals
Accessing a global variable:
configuses name lookup, not attribute lookup.
But accessing a module attribute:
settings.configuses attribute lookup on the module object.
Both are dictionary-backed and cacheable.
This is why imports and modules interact with lookup performance.
78.28 Attribute Access and Properties
Properties look like fields but behave like method calls.
class C:
@property
def value(self):
return compute_value()Access:
obj.valuecalls:
property.__get__which calls the getter.
This means property access can be arbitrarily expensive.
From a performance perspective, a property should be treated like a function call with attribute syntax.
78.29 Attribute Access and cached_property
functools.cached_property stores a computed value after the first lookup.
Conceptually:
first access:
compute value
store result in instance dict
later access:
read stored value from instance dictThis changes the performance profile from descriptor execution to dictionary lookup after the first access.
It is useful for expensive derived values on long-lived objects.
78.30 Attribute Access and Metaclasses
Class attribute access can involve metaclasses.
Example:
class Meta(type):
def __getattribute__(cls, name):
return super().__getattribute__(name)
class C(metaclass=Meta):
x = 1Access:
C.xuses lookup rules involving the metaclass.
Metaclasses make class objects behave dynamically. They also complicate attribute caching.
78.31 Attribute Access and Modules With __getattr__
Modules can define __getattr__.
Example:
# module file
def __getattr__(name):
...This allows lazy or dynamic module attributes.
But it means module attribute access may execute custom code on misses.
CPython must preserve this behavior, so caches must fall back correctly.
78.32 Attribute Access and C Extension Types
C extension types can define custom attribute behavior through type slots.
Important hooks include:
tp_getattro
tp_setattro
tp_members
tp_getset
method tables
descriptorsA C extension type can provide highly optimized attribute access or highly dynamic custom access.
Built-in types often use C-level layouts and descriptors for speed.
78.33 Attribute Access and Built-in Types
Built-in types such as list, dict, str, and int do not usually have per-instance dictionaries.
Their attributes and methods are defined at the type level.
Example:
items.append
text.upper
mapping.getMethod descriptors on built-in types are optimized.
An immediate method call like:
items.append(x)can use specialized method lookup and call paths.
78.34 Attribute Access and Memory Locality
Performance depends on locality.
A generic attribute lookup may touch:
object header
type object
type dictionary
instance dictionary
descriptor object
base class dictionaries
inline cache entriesEach pointer chase can miss CPU caches.
Fast paths reduce pointer chasing by using cached offsets, versions, and direct references.
This is one reason slots and stable layouts can be faster.
78.35 Practical Performance Guidelines
Attribute access optimization suggests several practical rules.
| Pattern | Reason |
|---|---|
| Prefer stable object shapes in hot loops | Helps inline caches specialize |
| Use local variables for repeated expensive lookups | Avoid repeated lookup work |
| Treat properties as function calls | They may execute arbitrary code |
Consider __slots__ for many small fixed-shape objects | Reduces memory and can speed access |
Avoid custom __getattribute__ unless necessary | It intercepts every attribute read |
| Avoid megamorphic hot sites when performance matters | Cache misses reduce specialization benefit |
Example:
append = items.append
for x in source:
append(x)This can avoid repeated method lookup in some cases. Modern CPython already optimizes many immediate method calls, so this should be measured before use.
78.36 Measuring Attribute Access
Use measurement rather than intuition.
Simple tools:
import timeit
timeit.timeit("obj.x", setup="class C: pass\nobj=C(); obj.x=1")For bytecode:
import dis
def f(obj):
return obj.x
dis.dis(f, adaptive=True, show_caches=True)This can show specialized forms and cache entries after warmup in supported Python versions.
78.37 Reading Attribute Access Source
Important CPython source areas:
| File | Purpose |
|---|---|
Objects/object.c | Generic attribute access helpers |
Objects/typeobject.c | Type lookup, descriptors, MRO behavior |
Objects/descrobject.c | Descriptor implementations |
Objects/dictobject.c | Instance and class dictionary behavior |
Python/ceval.c | Bytecode execution for attribute operations |
Python/specialize.c | Attribute specialization logic |
When reading the source, separate three levels:
semantic lookup rules
generic C implementation
specialized interpreter fast paths78.38 Mental Model
A useful model:
Attribute access is dynamic lookup with cached fast paths.The generic path preserves Python’s full object model.
The fast path exploits stable runtime shapes.
first executions:
generic lookup
hot stable site:
cached type and layout checks
cache hit:
direct load or descriptor-aware fast path
cache miss:
fallback to generic lookup78.39 Chapter Summary
Attribute access performance is central to CPython performance.
The operation:
obj.xmay involve dictionaries, descriptors, slots, inheritance, custom hooks, modules, metaclasses, and C extension slots.
Modern CPython improves common cases with:
inline caches
type version tags
dictionary version tags
specialized LOAD_ATTR forms
method-call fast paths
slot offset caching
split dictionariesFast attribute access depends on stable object shapes and cheap validation. Dynamic features remain fully supported, but they may force the interpreter back to the generic path.