Data and Non-Data Descriptor Precedence¶
Page Maps¶
flowchart TD
class["Static class value"] --> kind{"Descriptor kind?"}
kind -->|data| data["Descriptor wins before instance state"]
kind -->|non-data| state{"Same public name in instance dictionary?"}
state -->|yes| instance["Instance value wins"]
state -->|no| nondata["Descriptor __get__ runs"]
The protocol core identified which hooks exist. This core uses that classification to predict an ordinary instance read before executing it.
The storage owners do not change when lookup precedence changes:
flowchart TD
class["Delivery class"] --> data["shared data descriptor"]
class --> nondata["shared non-data descriptor"]
first["first Delivery instance"] --> firstdict["first instance dictionary"]
second["second Delivery instance"] --> seconddict["second instance dictionary"]
data -.reads or writes.-> firstdict
data -.reads or writes.-> seconddict
firstdict -.may shadow.-> nondata
seconddict -.may shadow.-> nondata
The claim¶
A data descriptor wins over a same-named instance dictionary entry. A non-data descriptor yields to that entry.
The distinction is not based on the value's purpose:
__set__or__delete__makes it a data descriptor__get__without those hooks makes it a non-data descriptor
Build the conflict deliberately¶
The lab creates this class:
class Delivery:
enforced = StoredField()
shadowable = ShadowableValue("descriptor-value")
category = "class-value"
Then it places two kinds of state on one instance:
delivery.enforced = "descriptor-storage"
vars(delivery)["enforced"] = "same-name-instance-value"
vars(delivery)["shadowable"] = "instance-value"
StoredField.__set__ wrote the managed value under _enforced. The explicit vars
assignment added a conflicting public entry named enforced. Both values exist, so the
read has to reveal the actual precedence rule.
Predict without executing¶
explain_lookup first uses inspect.getattr_static:
Unlike getattr, this does not call __get__. The helper can therefore inspect the
owner before producing a prediction:
if protocol.kind == "data":
winner = "data-descriptor"
elif name in vars(instance):
winner = "instance-dictionary"
elif protocol.kind == "non-data":
winner = "non-data-descriptor"
else:
winner = "class-attribute"
Run:
The precedence.decisions packet predicts:
| Name | Static kind | Same public instance name? | Winner |
|---|---|---|---|
enforced |
data descriptor | yes | data descriptor |
shadowable |
non-data descriptor | yes | instance dictionary |
category |
plain class value | no | class attribute |
The packet then records the actual reads so prediction and execution stay separate.
Trace the data-descriptor read¶
For delivery.enforced:
- Python finds
StoredFieldthroughDelivery's MRO. StoredField's type defines__set__.- The object is therefore a data descriptor.
- Its
__get__runs before Python considersvars(delivery)["enforced"]. __get__reads_enforcedand returns"descriptor-storage".
The conflicting public instance entry still exists. It simply does not win ordinary lookup.
Trace the non-data read¶
For delivery.shadowable:
- Python finds
ShadowableValuethrough the MRO. - Its type defines
__get__but not__set__or__delete__. - The object is a non-data descriptor.
- Python checks the instance dictionary before calling its
__get__. - The public instance entry returns
"instance-value".
Remove that instance entry and ShadowableValue.__get__ wins again.
Why a read-only property still wins¶
A property without a user-defined setter can feel read-only, but the property type
still supplies descriptor write machinery. Classification depends on the type's hooks,
not on whether a particular assignment succeeds.
This is why:
does not normally bypass a read-only property. The property is a data descriptor and is consulted before the same-named instance entry.
Why this does not justify subtle APIs¶
Precedence can explain an outcome without making the design kind.
If users must know that one class attribute is data and another is non-data merely to assign ordinary values safely, the public API may be too surprising. Use the rule for prediction and review, not as permission to make shadowing part of an undocumented contract.
Limits of the lab predictor¶
explain_lookup deliberately claims only ordinary lookup:
- it assumes the default
object.__getattribute__model - it does not execute
__getattr__ - it does not reproduce a custom
__getattribute__ - it does not model class lookup controlled by a custom metaclass
The ordinary_lookup_only evidence field keeps that limit machine-visible.
Capstone connection¶
Every capstone Field defines __get__ and __set__, so it is a data descriptor.
Generated initialization uses public assignment:
That routes through Field.__set__, which coerces and stores under a private name.
Later plugin.endpoint reads cannot be replaced by placing a public endpoint entry in
the instance dictionary. The data descriptor remains the owner.
This gives the capstone field system a strong invariant, but not an unlimited one:
direct mutation of the private storage name or object.__setattr__ can still bypass
parts of a public convention.
Focused proof¶
Run:
The tests include both sides of the conflict and a no-instance-entry non-data case.
Exit check¶
Before continuing, explain all three results without running them:
Your explanation must name the static class value, its category, whether the public name exists in the instance dictionary, and the winning layer.