Treat None as empty in optional set and list var operations
Merged
MA
4d ago+555+555+012345678901234567890123456789-57-57-01234567890123456789
Fixes #7826, fixes #7827, and fixes FINDING-036 from the reflex 0.11.0a1 pre-release testing (artifacts on branch claude/vibrant-pasteur-3921g3, prerelease-testing/runs/2026-11-11-v0.11.0a1/client_vars/ and verify_cv/).
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md?
- Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
- Bug fix (non-breaking change which fixes an issue)
- This change requires a documentation update (included)
Changes To Core Features:
- Have you added an explanation of what your changes do and why you'd like us to include them?
- Have you written new tests for your core changes, as applicable?
- Have you successfully ran tests with your changes locally?
Defect
Operations on a set[T] | None / frozenset[T] | None var holding None threw TypeError: Cannot read properties of null (reading 'some') in pySetHas, breaking the page in dev and failing the prod prerender (reflex export exit 1, Prerender: Request failed for /setnone/). SetVar is new in 0.11.0 (#7642); None in an optional set should act as an empty set.
Fix
utils/helpers/set.js: everypySet*helper reads a null/undefined operand (either side) as an empty set — one comparison on the non-null path, no allocation.SetVar.to_array()of an optional set compiles to(x ?? []), typedSequence[T], solength,rx.foreachand the deprecated array operations (which go throughto_array()) are null-safe too. A non-optional set compiles exactly as before.intersection/differenceof an optional set are typed non-optional (the helpers always return an array), andfrozenset[T] | Nonekeepsfrozensetthroughunion/symmetric_difference.ArrayVar(vars/sequence.py): every operation that reads the array (length,contains,join,reverse, slicing,+,*,pluck,map,filter,reduce,flat_map) goes through onearray_or_empty_operationview,(x ?? []), when the var type allows None (#7826).SetVar.to_array()reuses the same operation. Indexing was already null-safe (x?.at?.(i)), and a required list compiles exactly as before.ComputedVar/AsyncComputedVar.__get__: the list, tuple, set and frozenset overloads also accept the optional form, so-> set[str] | Nonereads asSetVar[set[str]]and-> list[str] | NoneasArrayVar[list[str]]for pyright (#7827).- Docs: the Set Operations section of
docs/vars/var-operations.mdstates the None-as-empty rule; the note is folded into the unreleased+set-var.feature.mdentry. The Contains, Reverse and Join section states the same rule for optional lists; two bugfix fragments cover the list and computed-var changes.
| op on a None set (either operand may be None) | result |
|---|---|
contains | false |
length | 0 |
union(x) / symmetric_difference(x) | x |
intersection / difference | [] |
issubset / isdisjoint | true |
issuperset(x) | true only when x is empty |
to_array() | [] |
rx.foreach / rx.cond / rx.match | no children / false branch / default |
Test plan
tests/js/set.test.js(new): every helper with null and undefined operands — before 8 failed / 31 passed (TypeError: Cannot read properties of null (reading 'some'),reading 'map'), after 39/39.tests/units/reflex_base/vars/test_set.py: compiled JS and result types of every operation on optional sets (state fields, computed vars, dataclass fields, either operand optional, frozenset), deprecated operations — before 11 failed, after 60 passed.tests/integration/tests_playwright/test_set_operations.py: 14 optional-set outputs while unset, after assignment and after a reset to None; the fixture now usesapp_harness_env, so it runs in dev and prod (the bug was in the prod prerender) — before: dev[Reflex Frontend Exception] TypeError ... at pySetHas, prodCreating Production Build failed with exit code 1; after: 2 passed.tests/units/reflex_base/vars/test_sequence.py(new): 14 array operations onlist[T] | Noneandtuple[T, ...] | Nonecompile through(x ?? []), and a required list has no guard — before 29 failed, after 43 passed.tests/units/reflex_base/vars/test_base.py:assert_typeof optional list, tuple, set, frozenset and async computed vars — before pyright reported 6reportAssertTypeFailure, after 0 errors.tests/integration/tests_playwright/test_var_operations.py:length,containsandjoinof an optional list while None and while set — beforeTypeError: Cannot read properties of null (reading 'length'), after passed.- Full unit suite: 15661 passed, 66 skipped, 81.37% (the Playwright browser-driver module could not run locally for lack of a Chromium binary).
ruff,pyright reflex tests, pre-commit,reflex-release changelog-check: clean. - End to end (QA app, 28 optional-set outputs): dev and
reflex run --env prodpass with the set None, set and unset again; the prerendered HTML already holds the empty-set results;reflex export --frontend-onlyexits 0 (1 before).
Notes
maybe.to_array()is now never None (maybe.to_array() == Noneis false) — a change to an unreleased API.
Generated by Claude Code