# 43. Descriptors

# 43. Descriptors

A descriptor is an object that controls attribute access on another object. Descriptors are one of the main mechanisms behind Python’s object model. They explain how methods bind to instances, how `property` works, how `staticmethod` and `classmethod` work, how slots work, and how many CPython-level type operations connect to Python-level syntax.

At the language level, a descriptor is any object that defines one or more of these methods:

```python id="x7hcrg"
__get__(self, obj, objtype=None)
__set__(self, obj, value)
__delete__(self, obj)
```

An object with `__get__` is a descriptor.

An object with `__set__` or `__delete__` is a data descriptor.

The distinction between non-data descriptors and data descriptors controls lookup precedence.

## 43.1 Why Descriptors Exist

Python attribute access looks simple:

```python id="96l8i5"
obj.name
```

But this expression does not mean “read a field named `name` from memory.”

It means:

```text id="drqc0j"
ask the object's type how attribute lookup works
search descriptors and dictionaries in a defined order
possibly call descriptor methods
return the resulting object
```

Descriptors make attribute access programmable. They allow one object to mediate access to another object’s attribute.

Examples:

```python id="tkau0z"
obj.method
obj.property_name
Class.class_method
Class.static_method
obj.slot_name
```

All of these involve descriptor behavior.

## 43.2 Basic Descriptor Protocol

A descriptor defines `__get__`.

```python id="ct8qxy"
class Descriptor:
    def __get__(self, obj, objtype=None):
        return "computed value"
```

Use it as a class attribute:

```python id="d0fypg"
class Example:
    value = Descriptor()

e = Example()

print(e.value)
```

Output:

```text id="qz34l2"
computed value
```

The descriptor object is stored on the class:

```python id="csuohy"
print(Example.__dict__["value"])
```

But attribute access through the instance calls `Descriptor.__get__`.

## 43.3 Descriptor Arguments

The descriptor method receives:

| Argument | Meaning |
|---|---|
| `self` | The descriptor object |
| `obj` | The instance being accessed, or `None` for class access |
| `objtype` | The owner class |

Example:

```python id="v4lvjg"
class Descriptor:
    def __get__(self, obj, objtype=None):
        print("obj:", obj)
        print("objtype:", objtype)
        return 42

class Example:
    value = Descriptor()

e = Example()

print(e.value)
print(Example.value)
```

For instance access:

```text id="by7jq8"
obj: <__main__.Example object at ...>
objtype: <class '__main__.Example'>
```

For class access:

```text id="k4ktgr"
obj: None
objtype: <class '__main__.Example'>
```

A descriptor can use this difference to return different values for instance and class access.

## 43.4 Non-Data Descriptors

A non-data descriptor defines `__get__`, but not `__set__` or `__delete__`.

```python id="hb1iji"
class NonDataDescriptor:
    def __get__(self, obj, objtype=None):
        return "descriptor value"
```

Example:

```python id="bvccxu"
class Example:
    value = NonDataDescriptor()

e = Example()

print(e.value)
```

Output:

```text id="j61t3h"
descriptor value
```

But because it is a non-data descriptor, an instance dictionary entry can override it:

```python id="vu3t3a"
e.__dict__["value"] = "instance value"

print(e.value)
```

Output:

```text id="6xj4jn"
instance value
```

This precedence is intentional. It is how normal methods can be shadowed by instance attributes.

## 43.5 Data Descriptors

A data descriptor defines `__set__` or `__delete__`.

```python id="rk5sgr"
class DataDescriptor:
    def __get__(self, obj, objtype=None):
        return "descriptor value"

    def __set__(self, obj, value):
        print("setting", value)
```

Example:

```python id="c9rzsd"
class Example:
    value = DataDescriptor()

e = Example()
e.__dict__["value"] = "instance value"

print(e.value)
```

Output:

```text id="m34bnr"
descriptor value
```

The data descriptor wins over the instance dictionary.

This rule is critical for `property`.

## 43.6 Lookup Precedence

For normal instance attribute access:

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

CPython’s object lookup roughly follows this order:

```text id="rpb90z"
1. Look for name on the type or base types.
2. If found and it is a data descriptor, call its __get__.
3. Look in the instance dictionary.
4. If found on the type and it is a non-data descriptor, call its __get__.
5. If found on the type as a normal attribute, return it.
6. If not found, call __getattr__ if defined.
7. Otherwise raise AttributeError.
```

This order explains why some class attributes can be shadowed by instance attributes and others cannot.

## 43.7 Descriptor Precedence Example

```python id="n4y91b"
class NonData:
    def __get__(self, obj, objtype=None):
        return "non-data descriptor"

class Data:
    def __get__(self, obj, objtype=None):
        return "data descriptor"

    def __set__(self, obj, value):
        raise AttributeError("read-only")

class Example:
    a = NonData()
    b = Data()

e = Example()
e.__dict__["a"] = "instance a"
e.__dict__["b"] = "instance b"

print(e.a)
print(e.b)
```

Output:

```text id="0dzdun"
instance a
data descriptor
```

`a` is shadowed because it is a non-data descriptor.

`b` is not shadowed because it is a data descriptor.

## 43.8 Functions Are Descriptors

Functions stored on classes are descriptors.

```python id="4040yq"
class Example:
    def method(self):
        return 42

e = Example()

print(Example.__dict__["method"])
print(e.method)
```

The object stored in the class dictionary is a function object.

The object returned by instance access is a bound method.

```text id="j0sg2u"
function object stored on class
    ↓ __get__(instance, class)
bound method object
```

This is why:

```python id="r1yoxy"
e.method()
```

passes `e` as the first argument.

## 43.9 Method Binding

A function’s descriptor behavior can be modeled like this:

```python id="rrmwg4"
class FunctionLike:
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return BoundMethod(self, obj)
```

The bound method stores:

```text id="9ac6b1"
function
instance
```

Calling the bound method:

```python id="zs0r2l"
e.method(1, 2)
```

is roughly equivalent to:

```python id="0jmqjg"
Example.method(e, 1, 2)
```

This is not special syntax in the parser. It is ordinary attribute lookup plus descriptor binding plus call.

## 43.10 Class Access to Methods

When accessed through the class, a function descriptor receives `obj=None`.

```python id="c1o0cb"
class Example:
    def method(self):
        return 42

print(Example.method)
```

The result is the original function-like object, not a bound method to an instance.

So this works:

```python id="ce7spz"
e = Example()

print(Example.method(e))
```

The instance is passed explicitly.

## 43.11 Bound Method Objects

A bound method object exposes useful attributes:

```python id="0hd310"
class Example:
    def method(self):
        return 42

e = Example()
m = e.method

print(m.__self__)
print(m.__func__)
```

`__self__` is the bound instance.

`__func__` is the underlying function.

Conceptually:

```text id="9rhbma"
bound_method.__self__ -> e
bound_method.__func__ -> Example.__dict__["method"]
```

Calling:

```python id="r7sscl"
m()
```

calls:

```python id="sqt0vq"
m.__func__(m.__self__)
```

## 43.12 `property`

`property` is a data descriptor.

```python id="o83sqw"
class User:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name
```

Access:

```python id="q15uwm"
u = User("Ada")
print(u.name)
```

calls the property’s getter.

A rough model:

```python id="e1wqf0"
class property:
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)
```

Because `property` defines `__set__`, it is a data descriptor. It wins over the instance dictionary.

## 43.13 Read-Only Properties

A read-only property still acts like a data descriptor.

```python id="bjmyqo"
class Example:
    @property
    def value(self):
        return 42

e = Example()
e.__dict__["value"] = 100

print(e.value)
```

Output:

```text id="8qw90w"
42
```

Even though the property has no setter, the `property` type has `__set__` behavior that raises an error. That makes it a data descriptor.

## 43.14 Writable Properties

A writable property adds a setter.

```python id="7362gp"
class User:
    def __init__(self):
        self._name = ""

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("empty name")
        self._name = value
```

Usage:

```python id="ly2z3r"
u = User()
u.name = "Ada"
print(u.name)
```

Assignment:

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

calls the descriptor’s `__set__`.

It does not simply write `u.__dict__["name"]`.

## 43.15 `staticmethod`

`staticmethod` is a descriptor that suppresses method binding.

```python id="7h9gwf"
class Math:
    @staticmethod
    def add(a, b):
        return a + b

print(Math.add(2, 3))
print(Math().add(2, 3))
```

In both cases, no instance is inserted as the first argument.

A rough model:

```python id="k4n2sv"
class staticmethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        return self.func
```

`staticmethod` stores a function and returns it unchanged.

## 43.16 `classmethod`

`classmethod` is a descriptor that binds the class instead of the instance.

```python id="m0y4q8"
class User:
    @classmethod
    def create(cls):
        return cls()

u = User.create()
```

A rough model:

```python id="vn2wvc"
class classmethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        return BoundMethod(self.func, objtype)
```

Calling:

```python id="cn0iaw"
User.create()
```

is roughly equivalent to:

```python id="mzdvxl"
User.__dict__["create"].__get__(None, User)()
```

which calls:

```python id="mgiurn"
original_function(User)
```

## 43.17 `__slots__` and Descriptors

`__slots__` uses descriptors to manage fixed-layout instance attributes.

Example:

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

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

The class dictionary contains slot descriptors:

```python id="ziiwfx"
print(Point.__dict__["x"])
print(Point.__dict__["y"])
```

These descriptors know how to read and write values from fixed offsets in the instance layout.

With slots, normal instances may not have `__dict__` unless explicitly requested.

```python id="mv07bu"
p = Point(1, 2)

print(hasattr(p, "__dict__"))
```

Output:

```text id="drcrll"
False
```

## 43.18 Member Descriptors and Getset Descriptors

CPython exposes some C-level descriptors as objects.

Common examples:

```text id="vtr7cq"
member_descriptor
getset_descriptor
wrapper_descriptor
method_descriptor
```

You can see them in class dictionaries:

```python id="7ln1br"
class Point:
    __slots__ = ("x",)

print(type(Point.__dict__["x"]))
```

For built-in types:

```python id="inwepf"
print(type(int.__dict__["real"]))
print(type(str.__dict__["upper"]))
print(type(object.__dict__["__str__"]))
```

These are CPython-level descriptor objects wrapping C-level behavior.

## 43.19 Descriptor and Attribute Assignment

For assignment:

```python id="4ja4xk"
obj.name = value
```

CPython does not always write into `obj.__dict__`.

If the type has a data descriptor for `name`, assignment calls the descriptor’s `__set__`.

Example:

```python id="2t9ndx"
class Descriptor:
    def __set__(self, obj, value):
        print("set", value)

class Example:
    x = Descriptor()

e = Example()
e.x = 10
```

Output:

```text id="jfd4bi"
set 10
```

No normal instance dictionary write is required.

## 43.20 Descriptor and Attribute Deletion

For deletion:

```python id="mxpuzk"
del obj.name
```

If a data descriptor defines `__delete__`, CPython calls it.

```python id="wuc0pw"
class Descriptor:
    def __delete__(self, obj):
        print("delete")

class Example:
    x = Descriptor()

e = Example()
del e.x
```

Output:

```text id="fkv43g"
delete
```

Otherwise, deletion may remove an entry from the instance dictionary or raise `AttributeError`.

## 43.21 `__set_name__`

Descriptors can define `__set_name__`.

```python id="yvbjoc"
class Field:
    def __set_name__(self, owner, name):
        self.owner = owner
        self.name = name

class User:
    id = Field()
    name = Field()
```

During class creation, Python calls:

```text id="zf52ho"
User.__dict__["id"].__set_name__(User, "id")
User.__dict__["name"].__set_name__(User, "name")
```

This lets descriptors learn the attribute name they were assigned to.

Without `__set_name__`, descriptors often require redundant configuration:

```python id="k9fsdd"
id = Field("id")
name = Field("name")
```

## 43.22 Practical Validating Descriptor

```python id="okxfod"
class PositiveInt:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError("expected int")
        if value <= 0:
            raise ValueError("expected positive integer")
        setattr(obj, self.name, value)
```

Usage:

```python id="u85219"
class User:
    age = PositiveInt()

    def __init__(self, age):
        self.age = age

u = User(30)
print(u.age)
```

The assignment:

```python id="7h7peb"
self.age = age
```

calls `PositiveInt.__set__`.

The access:

```python id="jvsgys"
u.age
```

calls `PositiveInt.__get__`.

## 43.23 Descriptor Storage Choices

Descriptors usually store per-instance values somewhere else.

Common choices:

| Storage | Example | Tradeoff |
|---|---|---|
| Instance dictionary | `obj.__dict__[name]` | Simple, requires `__dict__` |
| Private attribute | `obj._name` | Simple, can collide |
| Weak key dictionary | `WeakKeyDictionary[obj]` | Works without touching instance dict, higher overhead |
| Slot offset | CPython internal | Fast, fixed layout |
| External store | ORM/session/state table | Useful for frameworks |

A descriptor object is usually shared by the class, so storing instance-specific data directly on the descriptor is usually wrong.

Bad:

```python id="httjyo"
class BadField:
    def __set__(self, obj, value):
        self.value = value
```

All instances share one descriptor object.

Better:

```python id="qgjw4l"
class Field:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __set__(self, obj, value):
        setattr(obj, self.name, value)
```

## 43.24 Descriptor Sharing

A descriptor is stored on the class.

```python id="poo2z7"
class Field:
    pass

class User:
    name = Field()
```

There is one `Field` object for `User.name`.

All `User` instances access the same descriptor object.

```python id="ghzk97"
print(User.__dict__["name"])
```

This is why descriptor state must be designed carefully.

Descriptor-level state is good for:

```text id="lzdzy9"
field name
validation rule
default configuration
metadata
owner class
```

Instance-level state belongs on the instance or in an external per-instance store.

## 43.25 Descriptors and Inheritance

Descriptors are inherited like other class attributes.

```python id="07zg83"
class Field:
    def __get__(self, obj, objtype=None):
        return "value"

class Base:
    x = Field()

class Child(Base):
    pass

print(Child().x)
```

Output:

```text id="23j8kz"
value
```

Lookup searches the method resolution order. If a descriptor is found in a base class, it participates in attribute access for child instances.

A subclass can override the descriptor by defining the same name.

```python id="jgv5bi"
class Child(Base):
    x = 100
```

Now:

```python id="a343w1"
print(Child().x)
```

returns:

```text id="sbrd9y"
100
```

unless other lookup rules apply.

## 43.26 Descriptors and `super`

`super()` also uses descriptor binding.

Example:

```python id="klco94"
class Base:
    def method(self):
        return "base"

class Child(Base):
    def method(self):
        return super().method()
```

The expression:

```python id="vz6j6v"
super().method
```

finds `method` on the next class in the MRO and binds it to the original instance.

That binding still uses descriptor logic.

## 43.27 Descriptors and `__getattribute__`

All normal attribute access goes through `__getattribute__`.

```python id="0h0zfk"
obj.name
```

calls roughly:

```python id="wrm9c8"
type(obj).__getattribute__(obj, "name")
```

The default implementation, `object.__getattribute__`, implements descriptor lookup.

If a class overrides `__getattribute__`, it can change or bypass descriptor behavior.

Example:

```python id="6hlojz"
class Example:
    @property
    def x(self):
        return 42

    def __getattribute__(self, name):
        return "intercepted"

e = Example()
print(e.x)
```

Output:

```text id="i8f18x"
intercepted
```

The property is never reached because `__getattribute__` intercepts everything.

## 43.28 Descriptors and `__getattr__`

`__getattr__` is only called after normal lookup fails.

```python id="1tg4g8"
class Example:
    def __getattr__(self, name):
        return "missing"

e = Example()
print(e.anything)
```

Output:

```text id="cafj83"
missing
```

If a descriptor exists and returns a value, `__getattr__` is not called.

Lookup order:

```text id="hpfu2m"
__getattribute__ runs first
descriptor rules happen inside normal __getattribute__
__getattr__ handles missing names only
```

## 43.29 Descriptors and Metaclasses

Class attribute access uses descriptor logic too, but the object being searched is a class object.

```python id="6g2hvo"
class Meta(type):
    @property
    def label(cls):
        return cls.__name__.lower()

class User(metaclass=Meta):
    pass

print(User.label)
```

Here, `label` is a descriptor on the metaclass. Accessing `User.label` invokes descriptor lookup on `type(User)`.

This is why metaclasses can define computed class attributes.

## 43.30 Descriptor Lookup on Classes

For:

```python id="bugwo3"
Class.attr
```

lookup is handled by `type.__getattribute__`.

The search happens on the class object and its metaclass.

If an attribute in the metaclass is a descriptor, it may bind with the class as the object.

This is how `classmethod`, metaclass properties, and many built-in type operations work.

Class lookup and instance lookup are related but not identical. Instance lookup searches the instance type’s MRO. Class lookup searches through metaclass machinery.

## 43.31 Descriptors in ORMs

Descriptors are common in ORMs.

Example shape:

```python id="gs116j"
class Column:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj._row[self.name]

    def __set__(self, obj, value):
        obj._row[self.name] = value
```

Usage:

```python id="3gj06d"
class User:
    id = Column()
    name = Column()

    def __init__(self, row):
        self._row = row
```

Then:

```python id="e6nz6o"
u = User({"id": 1, "name": "Ada"})
print(u.name)
u.name = "Grace"
```

The descriptor maps attribute access to row storage.

## 43.32 Descriptors in Validation Libraries

Validation frameworks use descriptors to enforce constraints.

```python id="kcyh88"
class String:
    def __set_name__(self, owner, name):
        self.storage = "_" + name

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError("expected str")
        setattr(obj, self.storage, value)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.storage)
```

Usage:

```python id="4vl57c"
class User:
    name = String()

    def __init__(self, name):
        self.name = name
```

The class definition declares the field. The descriptor enforces the rule.

## 43.33 Descriptors in Cached Computation

A cached property is a non-data descriptor.

```python id="yh220g"
class cached_property:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self

        value = self.func(obj)
        obj.__dict__[self.name] = value
        return value
```

Usage:

```python id="kxogrn"
class Data:
    @cached_property
    def total(self):
        print("computing")
        return 100

d = Data()
print(d.total)
print(d.total)
```

First access calls the descriptor and stores the value in the instance dictionary.

Second access returns the instance dictionary value because the descriptor is non-data.

This relies directly on descriptor precedence.

## 43.34 Data vs Non-Data Design Rule

Use a data descriptor when the descriptor must control writes or must not be overridden by instance attributes.

Use a non-data descriptor when instance-level caching or shadowing is desired.

| Descriptor kind | Defines | Instance dict can override? | Common example |
|---|---|---:|---|
| Non-data descriptor | `__get__` only | Yes | functions, cached property |
| Data descriptor | `__set__` or `__delete__` | No | property, slots, validating fields |

This one distinction explains many attribute access behaviors.

## 43.35 CPython Internal Slots for Descriptors

At the C level, descriptor behavior is represented through type slots.

The relevant operations correspond to:

```text id="3v2e6f"
tp_descr_get
tp_descr_set
```

A type that implements `tp_descr_get` can act like it has `__get__`.

A type that implements `tp_descr_set` can act like it has `__set__` or `__delete__`.

Built-in descriptors are often C objects whose types provide these slots.

This is how built-in methods, slot descriptors, getset descriptors, and wrapper descriptors integrate with normal Python lookup.

## 43.36 Attribute Lookup in CPython Terms

A simplified CPython-level lookup for `obj.name`:

```text id="m04p6t"
1. type = Py_TYPE(obj)
2. descr = lookup name in type MRO
3. if descr has tp_descr_get and tp_descr_set:
       return descr.__get__(obj, type)
4. if obj has dict and name in dict:
       return dict[name]
5. if descr has tp_descr_get:
       return descr.__get__(obj, type)
6. if descr exists:
       return descr
7. call fallback or raise AttributeError
```

This is the operational heart of descriptors.

## 43.37 Descriptor Errors

Descriptors should raise `AttributeError` for missing attributes when they want normal attribute fallback behavior.

Example:

```python id="xeje8w"
class Maybe:
    def __get__(self, obj, objtype=None):
        raise AttributeError("not available")
```

A descriptor that raises `AttributeError` may interact with `getattr`, `hasattr`, and fallback mechanisms.

Raise precise exceptions. Do not hide unrelated errors as `AttributeError` unless the attribute is genuinely unavailable.

## 43.38 Descriptor Introspection

To inspect a descriptor, access it through the class dictionary.

```python id="047r6b"
class Example:
    @property
    def value(self):
        return 42

print(Example.__dict__["value"])
```

Accessing through the class may call `__get__`:

```python id="2rtf24"
print(Example.value)
```

For `property`, class access returns the property object. But other descriptors may return computed values.

The safest way to retrieve the raw descriptor is usually:

```python id="gvvuc9"
vars(Example)["value"]
```

or:

```python id="h6m1qm"
Example.__dict__["value"]
```

## 43.39 Common Descriptor Bugs

| Bug | Cause | Fix |
|---|---|---|
| All instances share one value | Stored instance state on descriptor | Store state on instance or external per-instance map |
| Property ignored by custom class | Overrode `__getattribute__` incorrectly | Delegate to `object.__getattribute__` |
| Cached property does not cache | Descriptor is data descriptor | Make it non-data or write custom precedence logic |
| Instance attribute cannot override field | Descriptor defines `__set__` | Use non-data descriptor if override is desired |
| Descriptor lacks field name | Did not implement `__set_name__` | Add `__set_name__` or pass name explicitly |
| Class access breaks | `__get__` does not handle `obj is None` | Return descriptor or class-level object |

## 43.40 Key Points

Descriptors are objects that define `__get__`, `__set__`, or `__delete__`.

A non-data descriptor defines `__get__` only.

A data descriptor defines `__set__` or `__delete__`.

Data descriptors take precedence over instance dictionaries.

Non-data descriptors can be shadowed by instance dictionaries.

Functions are descriptors, which is why methods bind to instances.

`property`, `staticmethod`, `classmethod`, slots, built-in methods, and many CPython internals use descriptors.

Descriptors are implemented in CPython through descriptor slots on type objects.

Understanding descriptors is required for understanding methods, properties, slots, metaclasses, and attribute lookup.
