FAQ¶
Quick answers to the questions we get most often. If yours isn't here, check the troubleshooting guide or open an issue.
General¶
Is Wybthon production ready?
Not yet. The framework is pre-1.0 and the public API may shift between minor releases (old APIs are removed outright rather than shimmed). We recommend it for prototypes, internal tools, and learning projects today, and we'll relax this guidance as the API stabilizes.
Does it work outside the browser?
Everything except the real browser. Signals, memos, effects, async computations, actions, stores, forms, context, flow control, and VDOM construction run in plain CPython. wybthon.testing renders components into an in-memory DOM through the real reconciler and event delegation, so you can test them with pytest (see the testing guide), and wybthon.server renders them to HTML for prerendering and server rendering.
Which Python version do I need?
Python 3.14. Template strings (PEP 750) are part of the component API, and server rendering, tests, and tooling run on the same Python version as the browser. In the browser that means Pyodide 314 or newer; wyb init pins 314.0.6.
Why Python in the browser?
Wybthon lets data scientists, researchers, and tooling teams build interactive UIs without switching to TypeScript. Pyodide makes the scientific Python stack (NumPy, pandas, scikit-learn, and friends) available client-side, so you can render results without a server round-trip.
Why is there a virtual DOM if this is 'SolidJS for Python'?
Python has no JSX compiler to split static markup from dynamic parts, and every DOM call crosses the Python-to-JavaScript bridge. The VDOM is a rendering implementation detail: each reactive hole diffs only its own subtree, and the reconciler batches the resulting mutations through a JavaScript kernel in one bridge crossing. The reactive model is still fine-grained; a signal change re-runs the holes that read it, never whole components.
Pyodide and runtime¶
Which Pyodide version should I target?
Pin a single version per deployment with pyodide-version in wybthon.toml. Wybthon requires Pyodide 314 or newer (Python 3.14). Building with the same Python version as the runtime also lets wyb build ship precompiled bytecode.
How do I install Python packages from PyPI?
Use micropip inside Pyodide:
The package must be pure Python or available as a Pyodide-compatible wheel. See the Pyodide guide for details.
How do I call a JavaScript API from Python?
Import names from the js module yourself (Wybthon doesn't re-export it):
from js import fetch, window
window.alert("hello!")
async def load_users():
response = await fetch("/api/users")
return await response.json()
Convert Python objects with pyodide.ffi.to_js(...) when handing them to JS APIs that expect plain objects. For DOM nodes Wybthon rendered, prefer Ref and Element over document.querySelector.
Building and shipping apps¶
Do I need a bundler or a build step?
There's no JavaScript bundler, but there is a build step. wyb build packages Wybthon and your application into archives with a generated bootstrap, prerenders the routes you list, and turns dev mode off; any static host can serve the result. wyb dev runs the same build in development mode and reloads on change. See the deployment guide.
Can I lazy-load route components?
Yes; see lazy. It's backed by an async memo, so it integrates with Loading for declarative loading UIs and with Errored for load failures, and each lazy component has a .preload() method for warming the cache early.
Components and props¶
How do I declare a component's props?
Subclass Props and take one parameter annotated with it. Prop[T] fields are reactive; anything else is a plain field:
from collections.abc import Callable
from wybthon import Prop, Props, button, component, prop
class SaveButtonProps(Props):
label: Prop[str] = prop(default="Save")
on_save: Callable[[], None] | None = None
@component
def SaveButton(props: SaveButtonProps):
return button(props.label, on_click=props.on_save)
A component with no inputs takes no parameters. See Authoring patterns.
How do I pass a callback to a component?
Declare it as a plain field, such as on_save: Callable[[], None] | None = None. Reading props.on_save returns the function exactly as the parent passed it, and nothing calls it for you. Only Prop[T] fields are reactive.
Why does my type checker reject prop(0)?
Defaults must be passed by keyword: prop(default=0), or prop(default_factory=list) for a mutable value. Type checkers only recognize a field default given by keyword. Plain fields use ordinary defaults (= None).
How do I pass children?
Subclass ParentProps and place props.children in the tree. Callers use item syntax, Card(title="Hi")[p("Body")], or the children= keyword.
Do I need a mypy plugin?
No. Props uses PEP 681 dataclass_transform, so pyright, Pylance, and mypy check component calls out of the box. See the typing guide.
Where did Router, Link, and form_state go?
The core lives in wybthon; the router, forms, virtual lists, and scheduling helpers live in their own modules. Import them from wybthon.router, wybthon.forms, wybthon.virtual, and wybthon.scheduling:
Reactivity¶
Why didn't my component re-run after a signal changed?
Because components run once by design. A read in the component body captures the value at setup time. To stay reactive, place the accessor itself in the rendered tree (span(my_signal)), wrap the expression in a zero-arg lambda (span(lambda: f"{count()} items")), or derive it with create_memo. In dev mode Wybthon warns when a signal, memo, or prop is read at the top level of a component body; use .peek() when a one-time read is what you want.
Why does my signal still show the old value right after I set it?
Writes are staged. set_count(1) records the new value, and count() keeps returning the committed value until the next flush: a browser microtask, the end of an event handler, or an explicit flush. Functional updates see the staged value, so set_count(lambda n: n + 1) twice in one handler adds two. In tests and plain scripts, call flush() after your writes before asserting.
How do I batch multiple signal updates?
You don't need to; everything batches. Consecutive writes in one handler (or one synchronous block) coalesce into a single flush, so an effect that reads both fields runs once:
There is no batch() function.
Why didn't my effect run when I created it?
create_effect runs its first time on the next flush, after the DOM has been committed, so an effect created in a component body sees the mounted DOM. If you need code to run immediately during setup, just run it; if you need something after mount, use on_settled. create_render_effect runs immediately, but it's meant for rendering primitives.
How do I fetch data?
Write an async def and pass it to create_memo. Reads before the first value raise NotReadyError, which the nearest Loading boundary turns into fallback UI; later refetches run as transitions that hold the dependent UI on the previous state until the new value lands. Use is_pending for a refresh hint, latest to peek without suspending, and refresh or resolve to drive it imperatively. Mutations go through action, optionally with create_optimistic for instant UI.
How do I highlight the selected row without re-running every row?
Use a projection keyed by the selection. Each row reads only its own key, so a change notifies the old and new rows:
Why don't I see dev warnings in production?
wyb build turns dev mode off before your application imports, so production builds skip development checks and warnings. wyb dev keeps them on. Check the mode with is_dev_mode().
Next steps¶
- New here? Read Getting started.
- Looking for something to build? Browse the examples.
- Hit an unexpected error? Try the troubleshooting guide.