Wybthon¶
Wybthon is SolidJS for Python: a client-side single-page application (SPA) framework with SolidJS 2.0's fine-grained reactive model, a Pythonic API, and a runtime that lives in the browser through Pyodide.
If you can write Python, you can build interactive web apps with Wybthon. There's no JavaScript build pipeline and no JSX: markup is HTML in a Python template string.
What is Wybthon?¶
You write function components in Python and return HTML written as Python 3.14 template strings: html(t"<p>Count: {count()}</p>"). Like Solid's, Wybthon's compiler turns every dynamic expression in that markup into a getter, and each template literal into a native <template> that the browser clones per instance, so the expressions you interpolate are its only moving parts. Components run once. When a signal changes, only the text nodes, attributes, and reactive holes that read it update, and the renderer batches the resulting DOM mutations into a single crossing of the Python-to-JavaScript bridge.
The framework ships with everything you need to build a real app:
- Templates:
htmltakes a t-string of ordinary HTML, with interpolations for text, attributes, events, refs, spreads, and component tags.his the programmatic layer. - The compiler:
{count() * 2}and a component parameter such as{title}stay reactive without being wrapped, branches ofShowand its kin are built only when they show, and production builds never parse a template at run time. - Reactive primitives:
create_signal,create_memo, andcreate_effect, with automatic batching and typed accessors. - Async-first data: an
async defpassed tocreate_memois the fetching primitive, andcreate_deferredis the async memo that never holds the rest of the UI;LoadingandErroredboundaries handle the pending and failure states. actionwithcreate_optimisticandcreate_optimistic_storefor mutations.- Draft-first stores over dataclasses, dicts, and lists, typed as your own data (
create_store,reconcile,create_projection). - Flow control (
Show,For,Repeat,Switch,dynamic), callableContextobjects,Portal,lazy, andlazy_module. - Typed components: props declared as ordinary function parameters, which pyright and mypy check against the function's own signature without a plugin.
- A router in
wybthon.router, as in Solid Router 2.0:create_routerwithRoutes orfile_routes, plain<a href>links, and preloading on hover. - Full-stack apps:
server_functionruns anasync defon the server wherever it's called,queryandform_actionread and change data (forms work before Python boots), andwybthon.asgiserves pages per request under any ASGI server. - Server rendering: prerender routes at build time or render per request in CPython, stream
Loadingboundaries, set the status and headers withhttp_statusandhttp_header, andhydratethe result in the browser. - Form state, validators, and accessibility helpers in
wybthon.forms. - A test renderer,
wybthon.testing, that mounts components into an in-memory DOM in plain CPython, and a pytest plugin that compiles templates in test modules. - A dev server (
wyb dev) that replaces edited modules in the open page without reloading it, production builds (wyb build) with templates compiled ahead of time, dev-mode checks turned off, the asset manifest inlined, the runtime preloaded, and boot snapshots that make repeat visits start in about a tenth of the time, andwyb runfor scripts.
Try it in 30 seconds¶
The smallest interactive Wybthon component looks like this:
from wybthon import component, create_signal, html, render
@component
def Counter():
count, set_count = create_signal(0)
def increment():
set_count(lambda n: n + 1)
return html(t"""
<div>
<p>Count: {count()}</p>
<button onclick={increment}>Increment</button>
</div>
""")
render(Counter(), "#app")
t"..." is a PEP 750 template string, new in Python 3.14. {count()} calls something, so the compiler turns it into a getter: a reactive binding, and only that text node updates when the signal changes. Walk through this example end to end in Getting started, read the Templates guide, or jump straight into the Concepts section.
Quickstart¶
-
Install Wybthon (Python 3.14 or newer):
-
Create a project and run the dev server, which updates the page as you edit:
-
Explore the demo apps and the API in the Concepts and API sections.
Why Wybthon?¶
- Run-once components and typed props. A
@componentdeclares its props as ordinary parameters with annotations and defaults, and the compiler makes every read of a parameter a tracked read of the parent's current value, as reading Solid's props object is. Use it in the markup to bind it, in a memo or effect to derive from it, or inuntrackfor a one-time read. Callbacks are parameters annotated as callable, passed through untouched. - A compiler, as in Solid. The compiler rewrites each template before its module runs: an expression that calls something, reads an attribute, or indexes becomes a getter, a component call in the markup receives getters for its props, and the content of
Show,Loading, and the other flow controls is built only when it shows. You write{count() * 2}, not a lambda. - Markup compiles ahead of time. Each template literal is parsed, validated, and turned into a mount function when its module is compiled; every instance clones its native
<template>with one kernel command and fills its slots. Production builds never parse a template or generate code at run time. - Holes are the unit of update. Each dynamic expression in the markup becomes its own binding. There are no component re-renders to reason about.
- Components and control flow cost little more than their markup. A component tag mounts the template its body returns straight into its slot, a
ShoworForthat fills a slot creates no DOM nodes of its own, a constant prop subscribes to nothing, and a binding that reads nothing reactive allocates no computation.Show,For,Repeat, andSwitchare native regions rather than wrapper components. - Automatic batching. Signal writes are staged and applied once per microtask (and at the end of every event handler). There's no
batch(); callflushonly when you need the settled state synchronously. - Async is part of the graph. Reading an async memo before it resolves raises
NotReadyError, which the nearestLoadingboundary turns into fallback UI. Content stays mounted while pending, and a later refetch runs as a transition: the UI that depends on the change holds on the previous state until the new value lands, so nothing tears. - A virtual DOM where it pays off. Every DOM call crosses the Pyodide bridge, so Wybthon emits DOM operations into a buffer and ships them to a JavaScript kernel in one batch per flush. Only reactive holes that return new markup are diffed, and a template from the same literal is patched slot by slot.
- Dev-mode diagnostics. Writing a signal inside a tracking scope raises
WriteInScopeError; reading a signal or prop at the top level of a component body warns, because that read isn't tracked; an unknown or missing prop raisesTypeError; a template that reachedhtml()without the compiler raisesTemplateError. Production builds turn these checks off. - Runs anywhere Python runs. The reactive core and the VDOM are pure Python, so component tests run in CPython with
wybthon.testing.
Documentation map¶
- Get started: install, write your first component, explore the dev server.
- Concepts: deep dives into the mental model, templates, the compiler, reactivity, components, lifecycle, VDOM, and DOM interop.
- Guides: task-oriented recipes for authoring patterns, testing, performance, typing, deployment, and more.
- Examples: complete, runnable modules for a counter, async fetch, forms, error handling, and routing.
- API reference: auto-generated documentation per module via
mkdocstrings. - Meta: contribution guide, documentation style guide, FAQ, and troubleshooting.
Next steps¶
- New to Wybthon? Start with Getting started.
- Coming from React or SolidJS? Read Mental model and the migration guides (from React, from Solid).
- Review runtime contracts for state visibility, ownership, and async behavior.
- Looking for an API symbol? Use the search box at the top of the page or jump to the API reference.