Problem
Every attribute access on a state instance goes through BaseState._get_attribute (__getattribute__), and every write through BaseState.__setattr__. Together they route inherited vars to the parent instance, keep backend vars in a side dict (_backend_vars), wrap mutables in MutableProxy, bind event handlers, track dirtiness, and enforce name-collision rules. That makes state access slow and couples all var behavior to BaseState.
Measured on Python 3.14 (current → bare descriptor prototype):
| op |
today |
descriptor |
| read var |
1050 ns |
255 ns |
| read inherited var |
1550 ns |
305 ns |
| read method |
410 ns |
45 ns |
| write var |
9760 ns |
290 ns |
Plan
Make Field, ComputedVar and EventHandler real descriptors that own their behavior.
Field: data descriptor bound to the state class that stores it (owner). Class access returns the Var; instance access walks parent_state to the owner instance, reads __dict__, lazily materializes defaults, wraps mutables. __set__ stores and marks dirty (dirty set, parents' dirty_substates, dependent computed var caches). _x fields are backend: tracked, persisted, never sent to the client.
ComputedVar: same owner resolution, so inherited computed vars cache on the owner.
EventHandler: __get__ returns itself on the class, a bound method on the owner instance.
- Inheritance follows the MRO. Fields from mixins /
BaseState are rebound per concrete state. A substate redeclaring a field gets its own independent field.
- Collisions: keep the class-definition reserved-name check; drop the var-vs-var shadow errors.
- Typo guard:
SetUndefinedStateVarError kept via a guard-only __setattr__ installed outside prod mode; prod has no __setattr__.
- Deltas:
get_delta and friends move to reflex/istate/delta.py; BaseState.get_delta becomes a deprecated wrapper.
- Dependency graph moves onto the descriptors.
Removes _get_attribute, __setattr__ routing, _backend_vars/backend_vars/inherited_vars/inherited_backend_vars, get_skip_vars, _fast_attr_names, is_backend_base_variable, and the shadow checks. Estimated ~700 lines removed, ~200 added.
Also fixes: in a background task on a substate, in-place mutation of an inherited mutable var marks the substate dirty instead of the owner, so the change is never sent or persisted.
Problem
Every attribute access on a state instance goes through
BaseState._get_attribute(__getattribute__), and every write throughBaseState.__setattr__. Together they route inherited vars to the parent instance, keep backend vars in a side dict (_backend_vars), wrap mutables inMutableProxy, bind event handlers, track dirtiness, and enforce name-collision rules. That makes state access slow and couples all var behavior toBaseState.Measured on Python 3.14 (current → bare descriptor prototype):
Plan
Make
Field,ComputedVarandEventHandlerreal descriptors that own their behavior.Field: data descriptor bound to the state class that stores it (owner). Class access returns theVar; instance access walksparent_stateto the owner instance, reads__dict__, lazily materializes defaults, wraps mutables.__set__stores and marks dirty (dirty set, parents'dirty_substates, dependent computed var caches)._xfields are backend: tracked, persisted, never sent to the client.ComputedVar: same owner resolution, so inherited computed vars cache on the owner.EventHandler:__get__returns itself on the class, a bound method on the owner instance.BaseStateare rebound per concrete state. A substate redeclaring a field gets its own independent field.SetUndefinedStateVarErrorkept via a guard-only__setattr__installed outside prod mode; prod has no__setattr__.get_deltaand friends move toreflex/istate/delta.py;BaseState.get_deltabecomes a deprecated wrapper.Removes
_get_attribute,__setattr__routing,_backend_vars/backend_vars/inherited_vars/inherited_backend_vars,get_skip_vars,_fast_attr_names,is_backend_base_variable, and the shadow checks. Estimated ~700 lines removed, ~200 added.Also fixes: in a background task on a substate, in-place mutation of an inherited mutable var marks the substate dirty instead of the owner, so the change is never sent or persisted.