Reusable Field Descriptors and Storage¶
Page Maps¶
flowchart LR
descriptor["One descriptor on the class"] --> config["Shared field configuration"]
descriptor --> first["first.__dict__"]
descriptor --> second["second.__dict__"]
first --> firstvalue["First value"]
second --> secondvalue["Second value"]
Descriptor reuse becomes useful only when the storage model remains honest. One descriptor object usually serves many owner instances, so configuration may be shared while managed values must remain independent.
The ownership split¶
Use this table before writing a field:
| State | Normal owner |
|---|---|
| public field name | descriptor configuration |
| validation or conversion options | descriptor configuration |
| derived private storage name | descriptor configuration |
| one object's current value | owner instance or explicit external store |
| class-family collection of fields | wider class machinery |
The phrase “the descriptor stores the value” is too vague for review. Name the exact container and key.
Instance dictionary storage¶
StoredField derives a private key during class creation:
Writes use the owner instance:
Reads use the same owner and key:
def __get__(self, instance, owner=None):
if instance is None:
return self
return vars(instance)[self.storage_name]
This shape is small, inspectable, and compatible with ordinary objects that have a
__dict__.
The two-instance proof¶
A storage design is not proven by one successful object.
The lab asserts:
first.endpoint = "https://one.test"
second.endpoint = "https://two.test"
assert vars(first) == {"_endpoint": "https://one.test"}
assert vars(second) == {"_endpoint": "https://two.test"}
The descriptor configuration is shared:
The values are not. This is the central field-storage invariant.
Failure: descriptor-held current values¶
Consider:
class BrokenField:
def __set__(self, instance, value):
self.value = value
def __get__(self, instance, owner=None):
if instance is None:
return self
return self.value
After:
both reads return "two". The descriptor followed the access protocol but violated
instance ownership.
Slotted owners change the storage choice¶
An owner with __slots__ may not have a dictionary. That does not mean values should move
onto the descriptor.
Possible designs include:
- require a dedicated slot known to the descriptor
- use an external
WeakKeyDictionary - reject incompatible owners at class creation
- choose a different abstraction
Each has a cost:
| Storage | Benefit | Cost |
|---|---|---|
| instance dictionary | simple and visible | unavailable on dictionary-free slots |
| dedicated slot | value remains on the owner | slot coordination must happen before class creation finishes |
WeakKeyDictionary |
external per-instance ownership without retaining owners | owners must support weak references and hashing |
| plain dictionary keyed by owner | straightforward external map | can keep owners alive |
dictionary keyed by id(owner) |
appears compact | stale entries and identity reuse make it unsafe |
Do not present weak-reference storage as a universal upgrade. It introduces its own owner requirements and concurrency questions.
Defaults are also storage policy¶
The capstone Field writes a default into each instance during generated initialization.
That is safe for its current immutable scalar defaults.
A mutable default would be a different contract:
Writing the same list object into every owner dictionary still shares state even though the dictionary keys are independent. A production field system would need a default factory or an explicit immutable-default restriction.
The descriptor protocol does not solve default identity.
Validation and storage are separate decisions¶
A field can correctly store per-instance values and still apply a bad conversion:
That may be appropriate for configuration input and inappropriate for strict domain types. Review these independently:
- Where is the value stored?
- Which inputs are accepted?
- Is conversion lossy?
- When does failure occur?
- Does reading mutate state?
The worked quantity descriptor will make all five visible.
Capstone field trace¶
For WebhookNotifier.endpoint:
sequenceDiagram
participant Init as generated __init__
participant F as StringField.__set__
participant P as plugin instance
Init->>F: setattr(plugin, "endpoint", raw)
F->>F: coerce and validate
F->>P: vars(plugin)["_endpoint"] = text
P->>F: plugin.endpoint
F-->>P: vars(plugin)["_endpoint"]
The metaclass generated the initializer, but the field descriptor owns coercion and storage access. The plugin instance owns the resulting value.
What the current proof establishes¶
The Module 07 tests establish:
- class access exposes shared descriptor configuration
- two instances retain different values
- public writes route into derived private keys
- unset reads fail as attributes rather than returning hidden sentinels
- the quantity example stores only canonical numeric values
They do not establish:
- thread safety for external storage
- support for dictionary-free slotted owners
- safe mutable defaults
- serialization compatibility
- cross-field validation
Those require additional owners and evidence.
Storage review checklist¶
For every reusable field, record:
- how many descriptor objects exist
- how many owner instances may use each descriptor
- the exact per-instance key or external map
- whether reads mutate storage
- whether defaults are copied, constructed, or shared
- what owner shapes are rejected
Focused proof¶
Run:
python3 -m unittest discover -s tests \
-p "test_descriptor_protocol_lab.py" -v
python3 -m unittest discover -s tests \
-p "test_quantity_descriptor_lab.py" -v
Then inspect two vars(instance) results. Do not infer storage from the public API alone.