Python Descriptors and __set_name__
Learn how Python descriptors customize attribute access, assignment, deletion, and class-created attribute metadata.
Advanced
A descriptor is an object that can change how an attribute behaves when that object is stored on a class. Descriptors power several features you have already seen, but you can also implement the protocol yourself when a class needs reusable attribute behavior.
Start with __get__
A descriptor becomes active through methods such as `__get__()`, `__set__()`, and `__delete__()`. For an instance access like `message.text`, Python can call the descriptor instead of simply returning a value from the instance dictionary.
|
|
The `text` class attribute is a descriptor. Accessing it through an instance calls `__get__()` with that instance; accessing it through the class passes `None` as the instance.
Understand descriptor arguments
The `instance` argument identifies the object being accessed. When the attribute is accessed through the class itself, Python passes `None` for `instance`; `owner` identifies the class involved in the lookup.
|
|
For `Report.status`, `instance` is `None`. For `report.status`, `instance` is the report object and `owner` is `Report`.
Learn the descriptor name
`__set_name__()` is called automatically during class creation for an object that defines the hook. This is useful when one descriptor implementation needs to know which class attribute name it manages.
|
|
When `User` is created, Python calls `StoredValue.__set_name__(User, "name")`, allowing the descriptor to build its private storage name automatically.
Make the descriptor manage assignment
Adding `__set__()` turns the descriptor into a data descriptor. That lets the descriptor validate or transform values before storing them.
|
|
Because the descriptor implements `__set__()`, it is a data descriptor and can control assignments to `quantity`.
See data-descriptor precedence
During instance lookup, a data descriptor takes priority over a value with the same name in the instance dictionary. This is what lets managed attributes enforce their rules consistently.
|
|
The data descriptor still supplies `item.value` even though the instance dictionary also contains a `value` key. Direct inspection of `__dict__` shows that the extra key is still there.
Compare non-data descriptors
A descriptor that defines `__get__()` but neither `__set__()` nor `__delete__()` is a non-data descriptor. An ordinary instance attribute can override it.
|
|
A descriptor with `__get__()` but without `__set__()` or `__delete__()` is a non-data descriptor, so an instance attribute can override it.
Control deletion
Descriptors can define `__delete__()` to decide what `del obj.name` means. The descriptor can remove stored state, reset it, or perform another deliberate action.
|
|
When a descriptor defines `__delete__()`, `del session.token` invokes that method instead of simply deleting an ordinary instance attribute.
Reuse one descriptor implementation
The same descriptor class can manage many different attributes. `__set_name__()` gives each descriptor instance the information it needs to choose separate storage.
|
|
Each descriptor instance receives its own attribute name through `__set_name__()`, so the same descriptor class can manage multiple fields.
Know the class-creation boundary
`__set_name__()` is automatic during class creation, not for later assignment. This boundary matters when descriptors are installed dynamically.
|
|
Python automatically calls `__set_name__()` while creating the class. If a descriptor is assigned later, the hook is not automatic; call it explicitly when the descriptor needs that initialization.
Keep the protocol in perspective
A descriptor needs to live on a class (or an inherited class) for the attribute-access machinery to invoke it. The descriptor protocol is different from the custom attribute-access hooks in the previous lesson: those hooks customize the instance's lookup methods, while a descriptor object changes the behavior of a particular class attribute.
You have already used built-in descriptor-backed features such as properties and method binding. This lesson explains the underlying protocol so you can recognize and implement it; it does not turn those earlier topics into new material.
Choose descriptors when behavior is reusable
A descriptor is useful when several classes or attributes need the same attribute behavior, such as validation, conversion, computed access, or controlled storage. For one simple attribute, an ordinary attribute or property is often easier to read.
Key takeaways
- A descriptor is an object that can customize attribute access when it is stored on a class.
- __get__() receives the instance and owner; instance is None when the attribute is accessed through the class.
- __set_name__() runs during class creation and gives a descriptor the owner class and assigned attribute name.
- A descriptor with __set__() or __delete__() is a data descriptor and takes precedence over an instance-dictionary value with the same name.
- A non-data descriptor can be overridden by an instance attribute, making descriptor precedence an important part of object behavior.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.