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 document
from crank import component, h
from crank.dom import renderer

@component
def 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:

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 document
from crank import component, h
from crank.dom import renderer

@component
def Greeting(ctx, props):
return h.p[f"Hello, {props['name']}!"]

@component
def 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 document
from crank import component, h
from crank.dom import renderer

@component
def Counter(ctx, props):
count = 0

@ctx.refresh
def increment():
nonlocal count
count += 1

for 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 asyncio
from js import document
from crank import component, h
from crank.dom import renderer

@component
async 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 component
from crank.template import jsx

@component
def 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.jsCrank.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.labelprops["label"]
onClickonclick

Learn More #

Edit on GitHub