# 78. Attribute Access Performance

# 78. Attribute Access Performance

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.

```python id="5bbd03"
obj.name
obj.name = value
del obj.name
```

At 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:

```python id="1e0362"
obj.x
```

may involve:

```text id="2fda1f"
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 exists
```

Attribute access is therefore a major performance target.

## 78.1 Why Attribute Access Matters

Python code uses attributes constantly.

Common examples:

```python id="7f2ec8"
user.name
request.headers
response.status_code
path.name
module.function
self.value
cls.registry
```

Many object-oriented programs are dominated by attribute and method access.

For example:

```python id="1cc1aa"
for item in items:
    total += item.price
```

The 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:

```c id="7a8b73"
p->x
```

This may become a direct load from memory.

In Python:

```python id="f9ac7b"
p.x
```

does not mean “load from a fixed field.” It means “perform Python attribute lookup.”

That lookup must support:

```text id="d78329"
instance attributes
class attributes
inherited attributes
properties
methods
slots
descriptors
custom __getattribute__
custom __getattr__
metaclasses
```

This 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:

```text id="a8ff1e"
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 AttributeError
```

This 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:

```python id="4aca92"
class C:
    @property
    def x(self):
        return 42
```

The `property` object is a descriptor.

For:

```python id="546f8e"
obj.x
```

CPython does not simply look in `obj.__dict__`. It finds the property on the class and calls its descriptor logic.

Conceptually:

```text id="bd2850"
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:

```python id="1f63aa"
class User:
    pass

u = User()
u.name = "Alice"
u.age = 30
```

The instance dictionary is roughly:

```python id="4c3e12"
{
    "name": "Alice",
    "age": 30,
}
```

Then:

```python id="7fe5c6"
u.name
```

usually becomes a dictionary lookup.

The dictionary key is the string `"name"`.

This is flexible because attributes can be added at runtime:

```python id="8716a8"
u.email = "a@example.com"
```

The cost is memory and lookup overhead.

## 78.6 Class Dictionaries

Classes also have dictionaries.

Example:

```python id="a66ba0"
class C:
    value = 10

    def method(self):
        return self.value
```

The class dictionary contains:

```text id="1fd0e0"
"value"  -> 10
"method" -> function object
```

When `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:

```python id="5a8d0f"
class A:
    x = 1

class B(A):
    pass

b = B()
print(b.x)
```

Lookup checks:

```text id="9a309d"
B
A
object
```

The 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:

```python id="f4e6c0"
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:

```python id="c67f9a"
class C:
    def __getattr__(self, name):
        return f"missing: {name}"
```

Then:

```python id="5d7250"
C().x
```

returns:

```text id="cbbda2"
missing: x
```

The 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:

```python id="e41a75"
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y
```

Instances 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:

```text id="e6b50e"
Point instance:
    slot 0 -> x
    slot 1 -> y
```

Attribute 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:

```python id="0b5422"
class Point:
    __slots__ = ("x", "y")
```

the class receives descriptor objects for `x` and `y`.

When CPython sees:

```python id="e8e1dd"
p.x
```

the slot descriptor knows the offset where the value is stored.

Specialized attribute access can cache that offset.

The fast path becomes:

```text id="d011d6"
check exact type
load slot value by offset
```

This is one of the fastest attribute access forms in CPython.

## 78.12 Method Access

A method access:

```python id="e36fcb"
obj.method
```

usually returns a bound method object.

The bound method stores:

```text id="27e418"
underlying function
bound instance
```

Example:

```python id="f98f58"
m = obj.method
m()
```

Here `m` must exist as a first-class object.

But for immediate calls:

```python id="af5efd"
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:

```python id="3ea669"
import math
math.sqrt(9)
```

The `math.sqrt` lookup is an attribute access on the module object.

Conceptually:

```text id="e537e5"
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.

```python id="7c3fc0"
obj.x = value
```

The assignment may:

```text id="a2a64d"
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 name
```

For 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.

```python id="0242dd"
del obj.x
```

This may:

```text id="88e1e5"
call a descriptor's __delete__
delete from the instance dictionary
clear a slot
raise AttributeError
```

Deletion 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:

```text id="fa49ef"
expected type pointer
expected type version
cached lookup result or offset
```

Fast path:

```text id="c4354f"
if type(obj) == cached_type
and cached_type version is unchanged:
    use cached lookup
else:
    fall back to generic lookup
```

This 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:

```text id="64a23d"
expected dictionary version
cached value
```

When 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:

```python id="ff9e01"
obj.x
```

the interpreter executes a `LOAD_ATTR`-family instruction.

Modern CPython can specialize this instruction based on observed runtime behavior.

Possible specialized paths include:

```text id="319b13"
instance dictionary value
slot value
module attribute
class attribute
method load
descriptor path
```

The 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:

```text id="418f56"
load obj
check Py_TYPE(obj) == cached_type
check type version unchanged
read value from cached offset
push result
```

This avoids:

```text id="5f674f"
hashing attribute name
searching dictionaries
walking MRO
running generic descriptor logic
```

The validation still protects semantics.

## 78.20 Cache Miss Path

A cache miss falls back to generic lookup.

Misses happen when:

```text id="436b10"
object type differs
class dictionary changed
instance dictionary layout changed
attribute was deleted
descriptor changed
custom lookup is involved
```

The miss path may also update the cache or cause deoptimization.

The key invariant:

```text id="7f39c7"
cache miss changes performance, not correctness
```

## 78.21 Attribute Access and Specialization

The adaptive interpreter observes each attribute access site.

Example:

```python id="aa2a9e"
def get_x(obj):
    return obj.x
```

At first, the `LOAD_ATTR` is generic or adaptive.

After repeated calls with the same shape:

```python id="d65f9c"
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:

```python id="e522ff"
for p in points:
    total += p.x
```

If 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:

```python id="b7c383"
def get_name(obj):
    return obj.name
```

called with:

```text id="f976e9"
User
Team
Project
Organization
```

A 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:

```python id="dad3ed"
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.

```python id="61b707"
obj.any_new_name = value
```

This flexibility has costs:

```text id="0a9ec9"
per-instance dictionary memory
hash table lookup
dictionary version management
cache invalidation
```

For 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:

```python id="3dbf8d"
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
```

Many instances share keys:

```text id="7899cf"
name
age
```

Each instance stores only values:

```text id="fb5a53"
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:

```python id="d0c6ce"
config
```

uses name lookup, not attribute lookup.

But accessing a module attribute:

```python id="f7f38b"
settings.config
```

uses 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.

```python id="cf60fb"
class C:
    @property
    def value(self):
        return compute_value()
```

Access:

```python id="b09d9c"
obj.value
```

calls:

```text id="b21c78"
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:

```text id="23e9a6"
first access:
    compute value
    store result in instance dict

later access:
    read stored value from instance dict
```

This 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:

```python id="2095d4"
class Meta(type):
    def __getattribute__(cls, name):
        return super().__getattribute__(name)

class C(metaclass=Meta):
    x = 1
```

Access:

```python id="0897d7"
C.x
```

uses 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:

```python id="dfc180"
# 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:

```text id="cf3fec"
tp_getattro
tp_setattro
tp_members
tp_getset
method tables
descriptors
```

A 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:

```python id="c5715e"
items.append
text.upper
mapping.get
```

Method descriptors on built-in types are optimized.

An immediate method call like:

```python id="19fcb2"
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:

```text id="8917d4"
object header
type object
type dictionary
instance dictionary
descriptor object
base class dictionaries
inline cache entries
```

Each 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:

```python id="1e8f2e"
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:

```python id="9a0d16"
import timeit

timeit.timeit("obj.x", setup="class C: pass\nobj=C(); obj.x=1")
```

For bytecode:

```python id="3aff97"
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:

```text id="b628f5"
semantic lookup rules
generic C implementation
specialized interpreter fast paths
```

## 78.38 Mental Model

A useful model:

```text id="4ae24c"
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.

```text id="e1eae9"
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 lookup
```

## 78.39 Chapter Summary

Attribute access performance is central to CPython performance.

The operation:

```python id="0acb26"
obj.x
```

may involve dictionaries, descriptors, slots, inheritance, custom hooks, modules, metaclasses, and C extension slots.

Modern CPython improves common cases with:

```text id="5c9ff0"
inline caches
type version tags
dictionary version tags
specialized LOAD_ATTR forms
method-call fast paths
slot offset caching
split dictionaries
```

Fast 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.
