Usage with Python
Crank.py lets you write Crank components in Python. It runs in the browser through PyScript, on either of its two Python runtimes: Pyodide (full CPython) or MicroPython (smaller and faster to load). Crank.py uses Crank.js for rendering, so components behave the same way as they do in JavaScript.
from js import documentfrom crank import component, hfrom crank.dom import renderer@componentdef Greeting():return h.div["Hello from Python!"]renderer.render(h(Greeting), document.body)
Setup #
Crank.py is on PyPI. Add it to your PyScript config, along with the Crank.js modules it uses:
<!DOCTYPE html><html><head><link rel="stylesheet" href="https://pyscript.net/releases/2026.7.3/core.css"><script type="module" src="https://pyscript.net/releases/2026.7.3/core.js"></script></head><body><py-config>packages = ["crankpy"][js_modules.main]"https://esm.run/@b9g/crank@0.7/crank.js" = "crank_core""https://esm.run/@b9g/crank@0.7/dom.js" = "crank_dom"</py-config><script type="py" src="./main.py"></script></body></html>
To use crank.async_ (Suspense, SuspenseList, lazy) or crank.html
(rendering to strings), also add the matching modules:
"https://esm.run/@b9g/crank@0.7/async.js" = "crank_async""https://esm.run/@b9g/crank@0.7/html.js" = "crank_html"
To use MicroPython, change py-config to mpy-config and type="py" to
type="mpy".
Pyperscript #
Crank.py builds elements with h, a Python stand-in for JSX. Props go in
parentheses and children go in square brackets.
h.div["Hello"] # <div>Hello</div>h.a(href="/about")["About"] # <a href="/about">About</a>h.input(type="text", required=True) # <input type="text" required />h.hr() # <hr />h.ul[h.li["One"],h.li["Two"],]h(MyComponent, name="Ada") # <MyComponent name="Ada" />h(MyComponent)[h.p["Child"]] # <MyComponent><p>Child</p></MyComponent>
A few rules make props work in Python:
- On HTML elements, underscores in prop names become hyphens:
data_test_id="x"becomesdata-test-id="x". Props passed to components keep their names. classis a Python keyword, so useclassName. For any other name that can’t be a keyword argument, spread a dict:h.div(**{"class": "box"}).- Use a double underscore for namespaced props:
h.div(prop__innerHTML=html)is<div prop:innerHTML={html} />. - A Python list is a fragment. For a fragment with a
key, useh("", key="a")["One", "Two"]. Fragment,Copy,Portal,Raw, andTextare importable fromcrank.
Components #
Decorate a function with @component to make it a component. A component
can take no arguments, a context (ctx), or a context and props
(ctx, props). Props are a Python dict.
from js import documentfrom crank import component, hfrom crank.dom import renderer@componentdef Greeting(ctx, props):return h.p[f"Hello, {props['name']}!"]@componentdef App():return h.div[h(Greeting, name="Ada"),h(Greeting, name="Grace"),]renderer.render(h(App), document.body)
Stateful Components #
Generator components keep state in local variables, the same way they do in
Crank.js. Loop over ctx to receive new props on each render, and use
nonlocal to change state from a callback. The @ctx.refresh decorator runs
the function and then re-renders the component.
from js import documentfrom crank import component, hfrom crank.dom import renderer@componentdef Counter(ctx, props):count = 0@ctx.refreshdef increment():nonlocal countcount += 1for props in ctx:yield h.div[h.p[f"{props['label']}: {count}"],h.button(onclick=increment)["Add one"],]renderer.render(h(Counter, label="Clicks"), document.body)
Event props use lowercase names, as in HTML: onclick, oninput,
onsubmit. A function decorated with @ctx.refresh may take the event as
an argument or take no arguments.
The @ctx.schedule, @ctx.after, and @ctx.cleanup decorators register
callbacks for the matching
lifecycle methods.
Async Components #
Components can be async functions. Use await to load data before
rendering.
import asynciofrom js import documentfrom crank import component, hfrom crank.dom import renderer@componentasync def Delayed(ctx, props):await asyncio.sleep(props["seconds"])return h.p[f"Waited {props['seconds']}s"]renderer.render(h.div[h(Delayed, seconds=1),h(Delayed, seconds=2),],document.body,)
On Pyodide, components can also be async generators, and async for props in
ctx works as it does in Crank.js. MicroPython can’t compile async generators,
so on MicroPython use a regular generator or an async function instead.
Template Tag #
On Python 3.14 and later, crank.template provides a jsx tag for
template strings (t-strings). It uses
the same syntax as the JSX template tag.
from crank import componentfrom crank.template import jsx@componentdef Greeting(ctx, props):for props in ctx:yield jsx(t"<p class='greeting'>Hello, {props['name']}!</p>")jsx(t"<{Greeting} name='Ada' />")
Differences from Crank.js #
| Crank.js | Crank.py |
|---|---|
<div class="a">Hi</div> | h.div(className="a")["Hi"] |
function *Counter() {} | @component + def Counter(ctx): with yield |
for ({label} of this) {} | for props in ctx: |
this.refresh(() => count++) | @ctx.refresh on a function that changes count |
props.label | props["label"] |
onClick | onclick |