Describe the bug
When a substate declares a field whose name matches a base var it inherits, the declaration is silently discarded. No warning, no error. The var keeps the parent's value and type, writes land on the parent, and class-level access returns the raw Python default instead of a Var — so using it in a component renders a static value rather than a reactive binding.
That substates can't redefine parent vars is a known limitation (#3820 asks for the capability). This issue is narrower and separate: whatever the answer there, the restriction is currently enforced silently and produces wrong values instead of raising.
Mechanism
get_skip_vars() includes set(cls.inherited_vars) (reflex/state.py:1114-1130), and base_vars filters on it (reflex/state.py:691-695):
cls.base_vars = {
name: get_var_for_field(cls, name, f)
for name, f in cls.get_fields().items()
if name not in cls.get_skip_vars() and f.is_var and not name.startswith("_")
}
So the field stays in cls.get_fields() but never becomes a base var. _init_var therefore never runs for it, leaving pydantic's raw default as the class attribute; and __getattribute__ (reflex/state.py:1531, the name in inherited_vars branch) delegates reads and writes to the parent.
Contrast with computed vars, where the same collision does raise — _check_overridden_basevars raises ComputedVarShadowsBaseVarsError. Base vars have no equivalent guard.
To Reproduce
import reflex as rx
from reflex.state import State
class Parent(State):
x: int = 1
class Child(Parent):
x: str = "ninety-nine" # silently ignored
root = State(_reflex_internal_init=True)
child = root.get_substate(Child.get_full_name().split(".")[1:])
parent = root.get_substate(Parent.get_full_name().split(".")[1:])
print(child.x) # 1 (int!) — not "ninety-nine", not even a str
child.x = 42
print(parent.x) # 42 — the write landed on the parent
print(child.dirty_vars) # set()
print(parent.dirty_vars) # {'x'}
print(repr(Child.x)) # 'ninety-nine' — a plain str, not a Var
Also observable without the type change (x: int = 99 on the child): Child.base_vars omits x, child.x is 1, and child.dict() has no x key.
Expected behavior
Either honor the redefinition (#3820), or reject it loudly — raise at class-creation time the way a shadowing computed var already does. The failure mode that shouldn't exist is the current one: the declared annotation and default are accepted by the type checker and by get_fields(), then discarded at runtime with no diagnostic.
The Child.x case is the most damaging in practice, because it fails without any exception: rx.text(Child.x) compiles a literal string into the page instead of a reactive var, so the value simply never updates.
Specifics
- Python Version: 3.14.7
- Reflex Version: 0.9.10.post33.dev0+065ddfef (
main @ 065ddfe)
- OS: Linux
Additional context
Long-standing, not a recent regression — the set(cls.inherited_vars) filter predates the current state.py layout. Surfaced while reviewing #7068, where the same path silently swallows a field named router; that PR doesn't change this behavior in either direction.
Related: #3820 (feature request for the capability), #1783 and #1368 (earlier, closed).
Describe the bug
When a substate declares a field whose name matches a base var it inherits, the declaration is silently discarded. No warning, no error. The var keeps the parent's value and type, writes land on the parent, and class-level access returns the raw Python default instead of a
Var— so using it in a component renders a static value rather than a reactive binding.That substates can't redefine parent vars is a known limitation (#3820 asks for the capability). This issue is narrower and separate: whatever the answer there, the restriction is currently enforced silently and produces wrong values instead of raising.
Mechanism
get_skip_vars()includesset(cls.inherited_vars)(reflex/state.py:1114-1130), andbase_varsfilters on it (reflex/state.py:691-695):So the field stays in
cls.get_fields()but never becomes a base var._init_vartherefore never runs for it, leaving pydantic's raw default as the class attribute; and__getattribute__(reflex/state.py:1531, thename in inherited_varsbranch) delegates reads and writes to the parent.Contrast with computed vars, where the same collision does raise —
_check_overridden_basevarsraisesComputedVarShadowsBaseVarsError. Base vars have no equivalent guard.To Reproduce
Also observable without the type change (
x: int = 99on the child):Child.base_varsomitsx,child.xis1, andchild.dict()has noxkey.Expected behavior
Either honor the redefinition (#3820), or reject it loudly — raise at class-creation time the way a shadowing computed var already does. The failure mode that shouldn't exist is the current one: the declared annotation and default are accepted by the type checker and by
get_fields(), then discarded at runtime with no diagnostic.The
Child.xcase is the most damaging in practice, because it fails without any exception:rx.text(Child.x)compiles a literal string into the page instead of a reactive var, so the value simply never updates.Specifics
main@ 065ddfe)Additional context
Long-standing, not a recent regression — the
set(cls.inherited_vars)filter predates the currentstate.pylayout. Surfaced while reviewing #7068, where the same path silently swallows a field namedrouter; that PR doesn't change this behavior in either direction.Related: #3820 (feature request for the capability), #1783 and #1368 (earlier, closed).