Skip to content

Add Field.set_default and point the state var assignment error at the declaring state - #7519

Merged
masenf merged 7 commits into
mainfrom
claude/brave-fermat-cp4qw5
Oct 8, 2026
Merged

masenf merged 7 commits into
mainfrom
claude/brave-fermat-cp4qw5

Conversation

@masenf

@masenf masenf commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #7516. It addresses @FarhanAliRaza's two post-merge review comments on the TypeError raised when a state var is assigned through its class.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Changes

Field.set_default(default=MISSING, *, default_factory=None) (comment). This is one supported way to change a state var's default. It takes exactly one of default (positional or keyword) or default_factory (keyword-only), and raises TypeError if it gets neither or both. As in Field and _default_arguments, a None factory means no factory.

  • An immutable default is stored as is.
  • A mutable default becomes a factory that returns a copy of the value as it was when set (it goes through _default_arguments, which Field._with_default also uses).
  • A default_factory replaces any default that is set.

Either way, default and default_factory stay mutually exclusive, so a stale default can no longer silently win over a new factory. Sessions never share a mutable default.

_default_arguments now copies a mutable value once up front. Instances read the default lazily, so without that copy, a caller who changed their list after set_default would change the default too. The same protection now applies to State.add_var, and a mutable class-declared default is deep-copied once when its class is created. Mutability is checked with types.is_immutable on the value itself only, not on items nested inside a tuple.

WatchlistState.__fields__["ticker"].set_default("MSFT")
WatchlistState.__fields__["symbols"].set_default(["AAPL", "MSFT"])
WatchlistState.__fields__["opened_at"].set_default(default_factory=time.time)

The error names the state that declares the var (comment). The message now names the field's _owner and points at the owner's field. When you assign through a substate, it also says the change applies to every state that inherits the var. Because set_default handles every kind of value, the suggestion is the same for all of them:

'count' is a state var of Parent, inherited by Child; assigning it on Child would replace the var.
Set its default with Parent.__fields__['count'].set_default(...), which applies to every state that
inherits it, or declare class-level config as ClassVar.

del Child.count (and the cleanup of a refused mock.patch.object) gets the same attribution. A state that mixes in a mixin's var owns its own copy of the field, so the error names that state, not the mixin. A substate of it names it as the declaring state. Only an assignment on the mixin class itself names the mixin.

Docs and tests now use set_default instead of assigning .default or .default_factory:

  • Docs: Changing Defaults, ComponentState, and the 0.10 upgrade guide.
  • Tests: ComponentState unit and integration tests, and the storage hydration test.

The upgrade guide also notes a 0.9 trap. On 0.9, assigning a mutable .default was safe because each instance got a copy. In 0.10 every instance shares the field's default, so a mutable value assigned there leaks changes between sessions.

Tests

  • test_field_set_default covers:
    • an immutable default, passed positionally and by keyword;
    • a mutable default replacing a set default with a copying factory, with no sharing between instances or with the caller;
    • an immutable default replacing a factory;
    • a default_factory replacing a default and being called for each instance;
    • None still working as a default.
  • test_field_set_default_takes_exactly_one covers the neither, explicit default_factory=None, and both cases.
  • The error tests (test_class_assignment_error_names_the_declaring_state, ..._names_the_state_mixing_in_a_var, ..._suggests_set_default) check the owner attribution and the uniform set_default(...) suggestion.
  • uv run pytest tests/units passes except tests/units/reflex_cli and tests/units/reflex_bench/drivers/test_browser.py, which fail the same way on main in my environment.
  • tests/integration/tests_playwright/test_hydration_storage.py passes (32 tests).
  • I couldn't run the Selenium tests/integration/test_component_state.py locally, because the container can't download a ChromeDriver.
  • ruff check, ruff format, and uv run pyright reflex tests are clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf

…the state var assignment error

Assigning a state var through a substate named the substate and suggested
setting the default on its __fields__, which hides that the field belongs to
the declaring state and that changing it applies to every state inheriting
it. The error now names the field's owner, points at the owner's field, and
says the change applies to inheriting states.

For a mutable value, a plain `.default = ...` shares one object across every
session, so the error suggests `default_factory = lambda: ...` instead, and
tells the user to clear a set default first, since a default wins over the
factory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
@masenf
masenf requested a review from a team as a code owner October 8, 2026 16:38
A state that mixes in a var owns its own copy of the field, so the error
names that state, and a substate of it names it as the declaring state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed against the latest diff

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Adds a new method to change state variable defaults.

The PR appears safe to merge; no new blocking issue was found.

What we checked:

  • Old factory stays active: set_default replaces both stored values. default_value returns None without calling a factory.

Summary

Adds Field.set_default and makes class-assignment errors name the state that declares the field. Updates docs and tests to use the new method.

  • Since the last review, default_factory=None now means no factory was supplied. The added tests match this behavior.
  • No new actionable issues or mounted-rule violations were found.
  • The previous unnumbered tuple finding is not reposted. masenf explicitly describes checking only the outer value, not items inside a tuple, as the intended behavior. Its thread was manually resolved without explanation.

Reviews (6) · Last reviewed commit: "Default set_default's default_factory to..." · Reviewed by Greptile

@codspeed

codspeed Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 7.35%

❌ 1 regressed benchmark
✅ 149 untouched benchmarks
⏩ 18 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_hydration_metadata[200] 1.7 ms 1.8 ms -7.35%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/brave-fermat-cp4qw5 (52e674f) with main (749dd55)

Open in CodSpeed

Footnotes

  1. 18 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

`Field.set_default(value)` sets a field's default in place the way
`_with_default` builds one: an immutable value becomes `default` and a mutable
one becomes a `default_factory` deep-copying it, so the two stay mutually
exclusive and sessions never share a mutable default. `_default_arguments`
now copies a mutable value once up front, so later changes by the caller do
not reach the default.

Since one call works for every value, the assignment error now suggests
`Owner.__fields__[name].set_default(...)` uniformly. Docs, the upgrade guide
and tests use set_default in place of assigning `.default` or
`.default_factory`, and the upgrade guide notes that the 0.9 idiom of
assigning a mutable `.default` now shares the value between sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
@masenf
masenf requested a review from Alek99 as a code owner October 8, 2026 17:11
@masenf masenf changed the title Name the declaring state and suggest a factory for mutable values in the state var assignment error Add Field.set_default and point the state var assignment error at the declaring state Oct 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

claude added 2 commits October 8, 2026 17:29
`set_default` now takes keyword-only `default` and `default_factory`, each
defaulting to MISSING, and raises TypeError unless exactly one is given. A
mutable `default` still becomes a factory returning a copy taken when it was
set; a `default_factory` replaces any set default. Docs, the error message and
tests use the keyword form, and the Changing Defaults docs show a factory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
The default value is the common case and the method name already says
"default", so `set_default("MSFT")` reads better than
`set_default(default="MSFT")`. `default_factory` stays keyword-only. Docs,
tests, news and the assignment error use the positional form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
Two plain conditions read better than comparing two booleans, and each case
now gets an error saying what was wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
Comment thread docs/vars/base_vars.md Resolved

@FarhanAliRaza FarhanAliRaza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this PR in a small Reflex app. The app calls set_default the way a user does. It sets an immutable default, a mutable list, a default_factory, an immutable value that replaces a declared factory, and an rx.LocalStorage value. It also has two ComponentState instances that call set_default in get_component.

In the browser, the new defaults show on first load. A change to the source list after set_default does not reach the default. reset() restores the new defaults, and the factory runs again on each reset. The storage var writes to its new storage name. The two ComponentState instances keep separate defaults. A second browser session gets clean defaults, so sessions do not share the mutable default. There are no console or backend errors.

I also checked the class-assignment error. It names the state that declares the var. For an inherited var, it says which states the change affects.

set_default resets default_factory to None, not to MISSING. This is correct, because None is the field's "no factory" value. A field with MISSING in that slot fails in default_value().

I left two inline comments.

…t_arguments

A field has no factory when its default_factory is None, as Field.__init__
and _default_arguments spell it, so set_default takes None for "not given"
too. An explicit default_factory=None counts as not given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
@masenf
masenf merged commit ee14ac5 into main Oct 8, 2026
145 of 146 checks passed
@masenf
masenf deleted the claude/brave-fermat-cp4qw5 branch October 8, 2026 18:55
masenf pushed a commit that referenced this pull request Oct 8, 2026
…9583012

Reconcile fragments main changed after the train materialized them:
carry #7519's `set_default` wording of the #7516 breaking entries into
the reflex and reflex-base a4 changelogs, and keep both materialized
fragments deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SjCrbdguMkfe4bhbaREiwo
masenf pushed a commit that referenced this pull request Oct 9, 2026
Curated from the a2-a5 passes, deduplicated and parameterized; the
Python suite, pytest suites, N-004 interchange and the cse2e app were
re-run on the released 0.10.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SjCrbdguMkfe4bhbaREiwo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants