# 33. Attribute Lookup

# 33. Attribute Lookup

Attribute lookup is the runtime process used to evaluate expressions such as:

```python id="5ob0m9"
obj.name
```

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

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

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

```python id="67bubm"
obj.x
```

asks CPython to look up the attribute named `"x"` on `obj`.

If found, the lookup returns a Python object.

If missing, it raises `AttributeError`.

Example:

```python id="l53g0p"
class C:
    pass

obj = C()
obj.x = 10

print(obj.x)
```

The assignment stores `x` in the instance dictionary:

```text id="kz7fxs"
obj.__dict__["x"] = 10
```

The access finds it there.

## 33.2 Attribute Lookup Is Not Dictionary Lookup Only

For simple objects, attribute access may look like dictionary lookup:

```python id="m2qolp"
obj.__dict__["x"]
```

But full attribute lookup is more complex.

This expression:

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

may involve:

```text id="h2b2fw"
data descriptors
instance dictionary
non-data descriptors
class attributes
base classes
__getattribute__
__getattr__
slots
properties
metaclass behavior
```

So this equivalence is incomplete:

```python id="ue4afg"
obj.x == obj.__dict__["x"]
```

It only holds in simple cases.

## 33.3 Instance Dictionaries

Most normal Python objects have an instance dictionary.

```python id="m08lyl"
class User:
    pass

u = User()
u.name = "Ada"
u.age = 37

print(u.__dict__)
```

Output:

```text id="7fbd65"
{'name': 'Ada', 'age': 37}
```

Instance attributes are stored in this dictionary unless the class uses slots or a custom layout.

For:

```python id="v04kyu"
u.name
```

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

```python id="f16lwf"
class User:
    kind = "human"

u = User()
print(u.kind)
```

Here, `kind` is not in `u.__dict__`. It is in `User.__dict__`.

Conceptually:

```text id="lpc7u3"
u.__dict__ does not contain "kind"
User.__dict__ contains "kind"
return "human"
```

Class attributes are shared through the class:

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

```python id="g0f5q9"
class User:
    kind = "human"

u = User()
u.kind = "admin"

print(u.kind)
print(User.kind)
```

Output:

```text id="la6r0l"
admin
human
```

Now:

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

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

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

The most important priority rule is:

```text id="rkgfxz"
data descriptor
    before instance dictionary

instance dictionary
    before non-data descriptor

non-data descriptor or class attribute
    after instance dictionary
```

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

```text id="hh6upi"
__get__
__set__
__delete__
```

Descriptors are stored on classes.

Example:

```python id="wgub37"
class Descriptor:
    def __get__(self, obj, cls):
        return "computed"

class C:
    x = Descriptor()

obj = C()
print(obj.x)
```

Accessing `obj.x` calls:

```python id="tbvfo8"
C.__dict__["x"].__get__(obj, C)
```

Descriptors allow classes to customize what attribute access means.

They power:

```text id="z7v9od"
methods
staticmethod
classmethod
property
slots
many built-in attributes
ORM fields
validation systems
lazy attributes
```

## 33.8 Data Descriptors

A data descriptor defines `__set__` or `__delete__`, usually with `__get__`.

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

```text id="gvfbh3"
from descriptor
```

The descriptor wins over the instance dictionary because it is a data descriptor.

This is how `property` works.

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

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

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

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

```python id="vxxcx9"
c.area
```

calls the property getter.

There is no explicit `()` in the source because the call is hidden inside descriptor access.

A setter adds assignment behavior:

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

```python id="acg7af"
u.name = " Ada "
```

calls the property setter.

## 33.11 Methods as Descriptors

Functions stored on classes are descriptors.

```python id="3l3xgm"
class C:
    def f(self):
        return 1

obj = C()
m = obj.f
```

The lookup:

```python id="fp1ymr"
obj.f
```

calls the function object’s descriptor logic and produces a bound method.

Conceptually:

```text id="1e8hzy"
C.__dict__["f"].__get__(obj, C)
    -> bound method
```

The bound method stores:

```text id="257g0u"
function = C.f
self = obj
```

Then:

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

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

```python id="0p2qg1"
class User:
    @classmethod
    def make(cls):
        return cls()
```

Calling:

```python id="jpqkoq"
User.make()
```

passes `User` as the first argument.

Calling through an instance also passes the class:

```python id="x77e2w"
User().make()
```

## 33.13 Slots

`__slots__` changes instance attribute storage.

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

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

Instances of `Point` usually do not have a normal `__dict__` unless explicitly requested.

```python id="jmosbt"
p = Point(1, 2)
print(p.x)
```

Slot attributes are implemented with descriptors stored on the class.

Conceptually:

```text id="cq58r6"
Point.__dict__["x"] = slot descriptor
Point.__dict__["y"] = slot descriptor
```

Accessing `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__`.

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

```text id="16oz5d"
lazy loading
proxy objects
RPC clients
compatibility layers
dynamic APIs
mock objects
```

If `__getattr__` also fails, it should raise `AttributeError`.

## 33.15 `__getattribute__`

`__getattribute__` intercepts all normal attribute access.

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

```python id="6fn49h"
class Bad:
    def __getattribute__(self, name):
        return self.__dict__[name]
```

Accessing `self.__dict__` calls `__getattribute__` again.

Use `object.__getattribute__` or `super().__getattribute__`:

```python id="qxi85u"
class Good:
    def __getattribute__(self, name):
        return object.__getattribute__(self, name)
```

## 33.16 Attribute Assignment

Assignment:

```python id="in3koz"
obj.x = value
```

uses attribute setting logic.

The rough order is:

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

A custom `__setattr__` can intercept assignment:

```python id="x89t2s"
class C:
    def __setattr__(self, name, value):
        print("set", name, value)
        super().__setattr__(name, value)

obj = C()
obj.x = 10
```

As with `__getattribute__`, careless code can recurse.

## 33.17 Attribute Deletion

Deletion:

```python id="annac7"
del obj.x
```

uses deletion logic.

It may call:

```text id="wvs69o"
type(obj).__delattr__
descriptor.__delete__
remove from instance dictionary
clear slot value
```

Example descriptor:

```python id="05vatk"
class D:
    def __delete__(self, obj):
        print("delete")

class C:
    x = D()

obj = C()
del obj.x
```

Deletion is part of the same descriptor and attribute protocol family.

## 33.18 Class Attribute Lookup

Classes are objects too.

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

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

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

```text id="3rcosr"
attributes on the class itself
descriptors in the metaclass
metaclass MRO
```

Metaclass lookup is separate from instance lookup, but it uses the same general object model.

## 33.20 Module Attribute Lookup

Modules have attribute dictionaries.

```python id="16mk8q"
import math
print(math.pi)
```

This is roughly:

```text id="iw6moh"
math.__dict__["pi"]
```

Modules can also define `__getattr__` for missing attributes.

```python id="a4cpwz"
# module.py
def __getattr__(name):
    if name == "lazy":
        return load_lazy()
    raise AttributeError(name)
```

Then:

```python id="pme0mb"
module.lazy
```

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

```python id="nri7kz"
class A:
    x = "A"

class B(A):
    pass

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

Lookup searches:

```text id="3gw1ky"
B
A
object
```

The order is stored in:

```python id="il7bpl"
print(B.__mro__)
```

Multiple inheritance uses C3 linearization to compute a consistent MRO.

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

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

```text id="zj3hcg"
MRO: Child, Base, object
super from Child
search Base, then object
bind result to self
```

`super()` 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:

```python id="2r7t8j"
len(obj)
```

uses the type’s length slot. It does not simply perform:

```python id="5oiizr"
obj.__len__()
```

This distinction matters:

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

```text id="wifw5q"
LOAD_ATTR
STORE_ATTR
DELETE_ATTR
LOAD_METHOD
```

For:

```python id="it1gq1"
value = obj.x
```

conceptual bytecode:

```text id="gp7253"
LOAD_FAST obj
LOAD_ATTR x
STORE_FAST value
```

For:

```python id="6xdbsd"
obj.x = value
```

conceptual bytecode:

```text id="id6s81"
LOAD_FAST value
LOAD_FAST obj
STORE_ATTR x
```

For method calls:

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

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

can call user code through:

```text id="tqt8ka"
__getattribute__
descriptor __get__
property getter
__getattr__
metaclass hooks
module __getattr__
```

Therefore attribute access can:

```text id="gq9mxu"
raise exceptions
mutate state
perform I/O
allocate objects
return different values each time
call arbitrary Python code
```

Example:

```python id="lqpgto"
class C:
    @property
    def x(self):
        print("computed")
        return 10

obj = C()
obj.x
obj.x
```

The getter runs each time.

## 33.26 Attribute Lookup and Exceptions

If an attribute is missing, Python raises `AttributeError`.

```python id="u5hs78"
obj.missing
```

But attribute lookup can raise other exceptions too.

Example:

```python id="yxnfz8"
class C:
    @property
    def x(self):
        raise RuntimeError("failed")

obj = C()
obj.x
```

This raises `RuntimeError`, not `AttributeError`.

Only missing attributes should generally raise `AttributeError`. Tools such as `hasattr` depend on this convention.

```python id="glsscx"
hasattr(obj, "x")
```

works by attempting lookup and catching `AttributeError`.

## 33.27 Attribute Lookup and `hasattr`

`hasattr(obj, name)` calls attribute lookup.

```python id="sf1vip"
hasattr(obj, "x")
```

is roughly:

```python id="d7n5ls"
try:
    getattr(obj, "x")
except AttributeError:
    return False
else:
    return True
```

This means `hasattr` can execute user code.

If a property getter raises `RuntimeError`, `hasattr` does not treat that as a missing attribute.

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

```python id="uk5dyw"
getattr(obj, "name")
```

is equivalent to:

```python id="qatskj"
obj.name
```

when the attribute name is known statically.

It also supports a default:

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

```python id="j47b3p"
setattr(obj, "x", 10)
```

is equivalent to:

```python id="1q0s4t"
obj.x = 10
```

for a static name.

It still respects:

```text id="amsxsl"
__setattr__
data descriptors
slots
read-only attributes
```

So `setattr` is not a raw dictionary write.

## 33.30 Attribute Lookup and `delattr`

`delattr` performs dynamic attribute deletion.

```python id="xe7qva"
delattr(obj, "x")
```

is equivalent to:

```python id="mbgk3j"
del obj.x
```

It still respects:

```text id="gnvxp9"
__delattr__
descriptor __delete__
slot deletion
instance dictionary deletion
```

## 33.31 Attribute Lookup and Inline Caches

Attribute lookup is frequent, so CPython optimizes it.

A repeated access:

```python id="xtr2dq"
for obj in objects:
    total += obj.value
```

may hit the same attribute layout many times.

CPython can attach inline cache data to the bytecode instruction. The cache may store facts such as:

```text id="ooumb4"
expected receiver type
type version tag
dictionary version
descriptor result
slot offset
instance dictionary offset
```

On later executions:

```text id="ylpgjg"
if guards still hold:
    use fast path
else:
    fall back to generic lookup
```

This preserves dynamic semantics while accelerating stable cases.

## 33.32 Cache Invalidation

Python allows class mutation.

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

```text id="1kbbj7"
type identity
type version
dictionary version
descriptor kind
instance layout
```

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

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

Each `p` has the same type and likely similar attribute layout. This is a good case for specialization.

Dynamic changes can reduce cache effectiveness:

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

```text id="hr03df"
module namespace
class namespace
instance namespace
globals
builtins
```

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

```python id="z6frja"
import math
math.sqrt
```

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

```python id="3y1uib"
import package.submodule
```

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

```python id="pswgfz"
list.append
dict.get
str.upper
int.bit_length
```

These attributes are typically method descriptors implemented in C.

Access through an instance:

```python id="c2qhto"
[].append
```

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

```text id="yd6imc"
bound methods
property return values
descriptor return values
exception objects
strings or proxy objects in custom hooks
```

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

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

```text id="pbs3p5"
mutate the object
mutate the class
change descriptors
trigger garbage collection
raise exceptions
call back into the same object
```

The lookup implementation must tolerate this.

## 33.39 Attribute Lookup and Proxies

Proxy objects often implement custom attribute lookup.

```python id="gmm0jz"
class Proxy:
    def __init__(self, target):
        self._target = target

    def __getattr__(self, name):
        return getattr(self._target, name)
```

Then:

```python id="t9wo3d"
proxy.x
```

delegates to:

```python id="mwraoc"
target.x
```

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

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

```python id="gygh09"
user.name
```

does not read a plain attribute. It calls `Field.__get__`.

This allows libraries to implement:

```text id="m7h3nv"
validation
lazy loading
database column mapping
change tracking
computed fields
relationship loading
```

Descriptors turn attribute syntax into programmable access.

## 33.41 Attribute Lookup and Dataclasses

Dataclasses do not fundamentally change attribute lookup.

```python id="itowya"
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int
```

Instances usually store fields in the instance dictionary unless slots are enabled.

```python id="2es6x4"
p = Point(1, 2)
print(p.x)
```

This is ordinary instance attribute lookup.

With slots:

```python id="zkk0iv"
@dataclass(slots=True)
class Point:
    x: int
    y: int
```

field storage uses slots rather than a normal instance dictionary.

## 33.42 Attribute Lookup and Type Objects

Types are objects, and they also have attributes.

```python id="veiddt"
int.bit_length
str.upper
dict.items
```

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

```text id="7jbz3r"
object has type
type is also an object
type has metaclass
metaclass controls class attribute lookup
```

## 33.43 Attribute Lookup and Performance

Attribute access has more overhead than local variable access.

Compare:

```python id="2vbgf5"
x
```

inside a function, where `x` is local:

```text id="quibql"
LOAD_FAST
```

with:

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

which requires:

```text id="p6kgjq"
LOAD_FAST obj
LOAD_ATTR x
```

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

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

```python id="uacsdx"
try:
    value = obj.missing
except AttributeError:
    value = default
```

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

```python id="pqoskd"
class C:
    pass

obj = C()
obj.x = 1

print(vars(obj))
```

Use class dictionaries:

```python id="zw539h"
print(C.__dict__)
```

Use MRO:

```python id="oqmr19"
print(C.__mro__)
```

Use `inspect.getattr_static` to inspect attributes without triggering normal dynamic lookup in many cases:

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

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

```text id="vn6j5o"
custom __getattribute__
metaclasses
slots internals
C-level fast paths
reference counts
inline caches
error handling
module lookup
special method lookup
```

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

```python id="xp7o07"
class C:
    x = 1
```

Then add:

```python id="jdu7zp"
obj.x = 2
```

Then add a method:

```python id="yzo90g"
def f(self): ...
```

Then replace it with:

```python id="nku86v"
@property
def x(self): ...
```

Then add:

```python id="6ho4rb"
__getattr__
__getattribute__
__slots__
staticmethod
classmethod
multiple inheritance
metaclass
```

For each step, inspect:

```python id="eaahv5"
vars(obj)
C.__dict__
C.__mro__
type(obj)
```

And disassemble access sites:

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

```text id="t5lrtf"
data descriptor
instance dictionary
non-data descriptor
class attribute
__getattr__
AttributeError
```

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