️ Web apps in pure Python 🐍
# Coding Agent Guidelines
Reflex: Python web **framework** compiling to React. Monorepo using uv workspace — main package in `reflex/`, sub-packages in `packages/`, docs site in `docs/`.
## Workflow
1. **Plan first.** Ensure the task is well-defined before writing code. If unclear, work with the user to flesh out details. No sloppy/spaghetti code — every feature/fix must be clearly understood first.
2. **Bugfixes:** write a regression test that fails before writing the fix.
3. **After implementation:** act as an adversarial reviewer. Scrutinize the diff against all rules in this file. Call out numbered issues, then wait for the user to request followup changes.
## Commands
Use `uv` for everything — never bare `python` or `python3`.
```
uv sync # install deps
uv run pytest tests/units --cov --no-cov-on-fail --cov-report= # unit tests (>=72% coverage)
uv run pytest tests/integration # integration tests (slow)
uv run ruff check . # lint
uv run ruff format . # format
uv run pyright reflex tests # type check
uv run python scripts/check_min_deps.py # validate each package's declared minimum dep versions (pyright in isolated min-version envs; workspace siblings resolve from locally built wheels, all other deps from PyPI). CI passes --wheelhouse instead, reusing the build jobs' artifacts
uv run python scripts/check_min_deps.py --check-dev-pins [pkg] # fail if pkg (default: all) declares an unpublishable *.dev dependency pin (the publish workflow runs the same gate via `reflex-release check-dev-pins`)
uv run reflex-release sync # regenerate the release workflows after editing [tool.reflex-release] or the reflex-release templates
uv run python scripts/make_pyi.py # regenerate .pyi stubs
uv run pre-commit run --all-files # all pre-commit hooks
```
## Layout
```
reflex/ # main framework package (app, state, compiler, components, utils, istate)
packages/ # workspace sub-packages (reflex-base, reflex-components-*, reflex-docgen, reflex-components-internal)
tests/units/ # unit tests, mirrors source tree
tests/integration/ # Selenium integration tests (run in dev+prod modes)
tests_playwright/ # Playwright integration tests (preferred for new tests)
tests/benchmarks/ # performance benchmarks
docs/ # documentation site (separate workspace member)
```
## Code style
- Concise, robust code. Reflex is a framework used in many ways — handle edge cases without unnecessary complexity.
- Performance matters. Avoid suboptimal patterns (e.g. iterating a dict to find a value by identity). Suggest restructuring data/APIs if an operation can't be done efficiently.
- Don't add expensive workarounds (e.g. `isinstance` checks) to paper over type-level problems — fix the root cause instead.
- Don't repeat validation or be over-defensive; trust data that was already validated upstream.
- Think in CPU cycles: avoid unnecessary data copies, redundant allocations, and gratuitous indirection.
- Extract duplicated code into parameterized helpers.
- No block comments (`# --- Section ---`, `# ============`). Plain inline comments only.
- Be cautious creating new public APIs — they must be documented and supported long-term.
- Google-style docstrings on all functions: one-line summary, optional detail sentence(s), then Args/Returns (or Yields)/Raises.
- Prefer imports at the top of the module in isort order. Only use inline imports when necessary to avoid circular dependencies.
## Testing
- Write comprehensive tests for new/changed features; extend existing test files where possible.
- Test functions at module level, not wrapped in classes.
- **Unit tests:** `tests/units/`, run with `uv run pytest tests/units`.
- unit tests should primarily cover a single module, and should be named accordingly, including subdirectories (e.g. `tests/units/istate/test_manager.py` for `reflex/istate/manager.py`). For subpackages, also include the corresponding path below `src/` (e.g. `tests/units/reflex_base/event/test_context.py` for `packages/reflex-base/src/reflex_base/event/context.py`).
- **Integration tests:** prefer Playwright (`tests/integration/tests_playwright/`). Integration tests are slow — extend existing test apps rather than creating new ones for trivial functionality. Multiple test cases sharing one app is fine.
### Integration test patterns
Apps as factory functions, run via `AppHarness`:
```python
def SomeApp():
import reflex as rx
class State(rx.State):
value: str = ""
def index():
return rx.box(rx.text(State.value))
app = rx.App()
app.add_page(index)
@pytest.fixture(scope="module")
def some_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
with AppHarness.create(
root=tmp_path_factory.mktemp("some_app"), app_source=SomeApp
) as harness:
yield harness
```
Playwright tests use the `page` fixture and navigate to `harness.frontend_url`. Utilities in `tests/integration/utils.py` (polling, event ordering, storage).
## .pyi stubs
When adding/modifying components: `uv run python scripts/make_pyi.py`. Commit `pyi_hashes.json` (not `.pyi` files). If the diff removes many modules, run `uv sync`, delete `.pyi_generator_last_run`, and regenerate.
## CI workflows
Branch rules require one check per workflow, listed in
`.github/rulesets/main-required-checks.json`. Check names are matched literally —
no wildcards — so two rules follow:
- **A required workflow must not filter its `pull_request` trigger.** A workflow
a path filter skips never reports its checks, so a required check on it blocks
the merge forever. Filter in a `changes` job instead and gate the real jobs on
`if: needs.changes.outputs.run == 'true'` — a job skipped by `if:` reports as a
pass. `push` triggers may keep their filters; nothing gates a merge there. So
may a workflow that blocks no merge — absent from the ruleset and listed in
that test's `ADVISORY` — where the filter costs a run rather than a merge.
- **Every merge-blocking workflow ends in a gate job** named `<workflow>-gate`,
which collapses it into one check name that matrix expansion cannot move. The
exception is a workflow with one job whose name cannot drift — `pre-commit`,
`changelog` — which the ruleset requires by that name (`DIRECTLY_REQUIRED` in
the test). A gate looks like:
```yaml
unit-tests-gate:
needs: [changes, unit-tests, unit-tests-macos] # every other job
if: always() # not !cancelled(): a cancelled run would report a pass
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@... # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/ci_gate
with:
needs: ${{ toJSON(needs) }}
```
Adding a job means adding it to the gate's `needs`; adding a workflow means
adding its gate to the ruleset. `tests/units/test_workflow_gates.py` fails when
either drifts. Every matrix leg blocks through its gate, pre-release Python
versions included, so leave `continue-on-error` off a gated job. A workflow
disabled in the repository's Actions settings never reports, which no test can
see: disabling one means moving it to `ADVISORY` and out of the ruleset, or every
merge waits on it.
## Changelog fragments
User-facing changes need a news fragment in the `news/` directory of each
package they touch (the repo root's `news/` for `reflex`), named
`+<slug>.<type>.md`. Putting the PR number in the name (`<PR number>.<type>.md`)
is optional: the release process renames an orphan fragment when it can identify
the PR that added it, so its entry links to that PR. Don't push a commit just to
rename one. Types: `breaking`, `deprecation`, `feature`, `bugfix`,
`performance`, `docs`, `misc`.
Write for external downstream users, not for reviewers. Every entry links to
its PR, so motivation, narrative, and implementation details belong in the PR
and the commit message — a reader who wants them will follow the link. Keep the
fragment to a sentence or two saying what changed and what it means for a user:
> Reduce published wheel and sdist size by removing misplaced generated artifacts.
Brevity is about the narrative, not the substance: whatever is genuinely useful
downstream belongs in the fragment. A brief usage example for a new feature, or
the before/after of converting deprecated usage to the supported style, earns
its place. Once it runs past a few sentences and a small code block, it is
documentation — write it under `docs/` and let the fragment link there.
CI requires a fragment for every package whose source the PR touches; the
`skip-changelog` label waives it for changes that are genuinely not user-facing.
## Sibling dependency floors
When a package needs an unreleased change in a workspace sibling, raise its
floor on that sibling to the `.dev0` of the sibling's next version. Every
package derives its version with `bump = true`, so each commit after its newest
tag builds as a dev release of that next version:
- after `reflex-base-v0.9.12`: `reflex-base >= 0.9.13.dev0`
- after `reflex-components-core-v0.9.10.post1`: `reflex-components-core >= 0.9.10.post2.dev0`
Such a floor excludes every release up to that tag and is met by every later
commit, so check-min-deps passes. A post release of the sibling cut after the
floor was written leaves main building below it; re-floor at that post release.
Releasing the dependent lifts it to the first
published version that satisfies it (see `packages/reflex-release/README.md`,
"Dependency pins across a release"). A floor the workspace can't meet fails
check-min-deps with "builds as ... here".
## Breaking changes and deprecation
Reflex has downstream users — don't break them. Provide a fallback path during deprecation.
**Runtime warning** via `console.deprecate()`:
```python
from reflex_base.utils import console
console.deprecate(
feature_name="OldFeature",
reason="Use NewFeature instead.",
deprecation_version="<next dot version of latest git tag>",
removal_version="1.0",
)
```
Set `deprecation_version` to the next dot version of the latest tag (`git fetch --tags` if needed, e.g. tag `v0.7.3` -> `"0.7.4"`). Set `removal_version` to next major unless directed otherwise.
**Type-level deprecation** for deprecated methods/overloads using `typing_extensions.deprecated`, always inside a `TYPE_CHECKING` guard to avoid double warnings:
```python
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing_extensions import deprecated
@deprecated("Use new_method() instead")
def old_method(self) -> str: ...
```
## Checklist
Before submitting:
1. Tests pass with adequate coverage
2. `uv run ruff check .` and `uv run ruff format .` clean
3. `uv run pyright reflex tests` passes
4. `pyi_hashes.json` updated if components changed
5. Documentation updated if user-facing behavior changed
6. News fragment added for user-facing changes
7. Deprecation warnings added if breaking changes introduced