Skip to content
bikeshavingPublic

About

Python Frontend Framework with Async/Generators, Powered by Crank.js

Resources

Contributing

Stars

9 stars

Watchers

1 watching

Forks

Repository files navigation

⚙️🐍 Crank.py

Modern components for Python frontend development.

PyScript Compatible Pyodide Compatible MicroPython Compatible MIT License

Built on the Crank.js framework.

Features

  • Pythonic Hyperscript - Clean template h.div["content"] syntax inspired by JSX
  • Generator Components - Natural state management using Python generators
  • Async Components - Components can use async def/await and await for
  • Lifecycle Decorators - @ctx.refresh, @ctx.after, @ctx.cleanup
  • Dual Runtime - Works with both Pyodide (full support) and MicroPython (basic components)
  • Browser Native - No build step

Installation

PyScript

<py-config type="toml">
packages = ["crankpy"]

[js_modules.main]
"https://esm.run/@b9g/crank@latest/crank.js" = "crank_core"
"https://esm.run/@b9g/crank@latest/dom.js" = "crank_dom"
"https://esm.run/@b9g/crank@latest/async.js" = "crank_async"
"https://esm.run/@b9g/crank@latest/html.js" = "crank_html"
</py-config>

crank_async is needed for crank.async_ (Suspense, SuspenseList, lazy). crank_html is needed for crank.html (rendering to HTML strings).

pip

pip install crankpy

Quick Start

Hello World

from crank import h, component
from crank.dom import renderer
from js import document

@component
def Greeting(ctx):
    for _ in ctx:
        yield h.div["Hello, Crank.py!"]

renderer.render(h(Greeting), document.body)

Interactive Counter

@component
def Counter(ctx):
    count = 0

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

    @ctx.refresh
    def decrement():
        nonlocal count
        count -= 1

    for _ in ctx:
        yield h.div[
            h.h2[f"Count: {count}"],
            h.button(onclick=increment)["+"],
            h.button(onclick=decrement)["-"]
        ]

Props Reassignment

@component
async def UserProfile(ctx, props):
    old_user_id = None
    for props in ctx:  # Props automatically update!
        user_id = props.user_id
        if user_id != old_user_id:
            user = await fetch_user(user_id)  # Fetches when props change
            yield h.div[
                h.img(src=user.avatar),
                h.h2[user.name],
                h.p[user.bio]
            ]
        else:
            yield h(Copy)
        old_user_id = user_id

# Usage
h(UserProfile, user_id=123)

Hyperscript Syntax Guide

Crank.py uses a clean, Pythonic hyperscript syntax:

HTML Elements

# Simple text content
h.div["Hello World"]
h.p["Some text"]

# With props
h.input(type="text", value=text)
h.div(className="my-class")["Content"]

# No props or children
h.hr()
h.br()

# Snake_case → kebab-case conversion
h.div(
    data_test_id="button",     # becomes data-test-id
    aria_hidden="true"         # becomes aria-hidden
)["Content"]

# Props spreading (explicit + spread)
h.button(className="btn", **userProps)["Click me"]
h.input(type="text", required=True, **formProps)

# Multiple dict merging (when needed)
h.div(**{**defaults, **themeProps, **userProps})["Content"]

# Nested elements
h.ul[
    h.li["Item 1"],
    h.li["Item 2"],
    h.li[
        "Item with ",
        h.strong["nested"],
        " content"
    ]
]

# Style objects (snake_case → kebab-case)
h.div(style={
    "background_color": "#f0f0f0",  # becomes background-color
    "border_radius": "5px"          # becomes border-radius
})["Styled content"]

# Reserved keywords with spreading
h.div(**{"class": "container", **userProps})["Content"]
# Or better: use className instead of class
h.div(className="container", **userProps)["Content"]

Components

# Component without props
h(MyComponent)

# Component with props
h(MyComponent, name="Alice", count=42)

# Component with children
h(MyComponent)[
    h.p["Child content"]
]

# Component with props and children
h(MyComponent, title="Hello")[
    h.p["Child content"]
]

Fragments

# Simple fragments - just use Python lists!
["Multiple", "children", "without", "wrapper"]
[h.div["Item 1"], h.div["Item 2"]]

# Fragment with props (when you need keys, etc.)
h("", key="my-fragment")["Child 1", "Child 2"]

# In context
h.div[
    h.h1["Title"],
    [h.p["Para 1"], h.p["Para 2"]],  # Simple fragment
    h.footer["Footer"]
]

Template Syntax Guide

On Python 3.14 and later (and MicroPython v1.28.0 and later), you can write elements with template strings (t-strings) instead of h. The jsx tag in crank.template is a port of Crank’s JSX template tag. Values go in {expression}, Python’s syntax, instead of ${expression}. html is another name for the same tag.

from crank.template import jsx

jsx(t'<div class="greeting">Hello, {name}!</div>')

Tags

Write HTML tags as text. Put components and other values in braces:

jsx(t"<div />")                       # element
jsx(t"<{Greeting} name='Ada' />")     # component

A component’s closing tag can be written three ways:

jsx(t"<{Card}>Hi</{Card}>")           # must match the opening tag
jsx(t"<{Card}>Hi<//Card>")            # the text after // is not checked
jsx(t"<{Card}>Hi<//>")                # closes the nearest open tag

Props

jsx(t"<button disabled>Save</button>")    # boolean prop, set to True
jsx(t"<div class='box' />")               # string, in single or double quotes
jsx(t"<button onclick={save}>Save</button>")  # any Python value
jsx(t'<div class="card {theme}" />')      # values inside a string
jsx(t"<div ...{props} />")                # spread the items of a dict
jsx(t"<input prop:value={text} />")       # namespaced props

Prop names are used as written, so class, for, and data-id work directly. Unlike h, the template tag doesn’t change underscores to hyphens.

Children

Children can be text, {expressions}, and other elements. HTML comments are removed, along with any expressions inside them. Several elements at the top level become a fragment, and so does <>...</>.

jsx(t"""
    <ul>
        <!-- one item per todo -->
        {[jsx(t"<li key={todo['id']}>{todo['title']}</li>") for todo in todos]}
    </ul>
    {done and jsx(t"<p>All done!</p>")}
""")

Use a list comprehension to render a list, and and or a conditional expression to render something only some of the time. None and False render nothing.

For a fragment with a key, use the Fragment component:

from crank import Fragment

jsx(t"<{Fragment} key={term}><dt>{term}</dt><dd>{definition}</dd><//>")

Whitespace

Whitespace works as it does in JSX. A line break and the indentation around it are removed, so you can indent templates freely. Spaces within a line are kept.

Differences from the JavaScript Tag

  • Write a literal brace as {{ or }}.
  • Format specs and conversions work as in f-strings: {price:.2f}, {value!r}.
  • Python handles backslash escapes like \n before the tag sees the string.
  • Namespaced tag names like <svg:circle> aren’t supported. Namespaced prop names like xlink:href are.

The tag caches what it parses for each template, so calling jsx on every render is cheap. A mistake in a template raises a SyntaxError that shows the line and the position of the problem.

Component Lifecycle

Component Signatures

Crank.py supports three component signatures:

# 1. Static components (no state)
@component
def Logo():
    return h.div["⚙️ Crank.py"]

# 2. Context-only (internal state)
@component
def Timer(ctx):
    start_time = time.time()
    for _ in ctx:
        elapsed = time.time() - start_time
        yield h.div[f"Time: {elapsed:.1f}s"]

# 3. Context + Props (dynamic)
@component
def TodoItem(ctx, props):
    for props in ctx:  # New props each iteration
        todo = props.todo
        yield h.li[
            h.input(type="checkbox", checked=todo.done),
            h.span[todo.text]
        ]

Lifecycle Decorators

@component
def MyComponent(ctx):
    @ctx.refresh
    def handle_click():
        # Automatically triggers re-render
        pass

    @ctx.schedule
    def schedule_render():
        # Runs before the DOM nodes are inserted
        pass

    @ctx.after
    def after_render(node):
        # Runs after DOM updates
        node.style.color = "blue"

    @ctx.cleanup
    def on_unmount():
        # Cleanup when component unmounts
        clear_interval(timer)

    for _ in ctx:
        yield h.div(onclick=handle_click)["Click me"]

Examples

Todo App

@component
def TodoApp(ctx):
    todos = []
    new_todo = ""

    @ctx.refresh
    def add_todo():
        nonlocal todos, new_todo
        if new_todo.strip():
            todos.append({"text": new_todo, "done": False})
            new_todo = ""

    @ctx.refresh
    def toggle_todo(index):
        nonlocal todos
        todos[index]["done"] = not todos[index]["done"]

    for _ in ctx:
        yield h.div[
            h.h1["Todo List"],
            h.input(
                type="text",
                value=new_todo,
                oninput=lambda e: setattr(sys.modules[__name__], 'new_todo', e.target.value)
            ),
            h.button(onclick=add_todo)["Add"],
            h.ul[
                [h.li(key=i)[
                    h.input(
                        type="checkbox",
                        checked=todo["done"],
                        onchange=lambda i=i: toggle_todo(i)
                    ),
                    h.span[todo["text"]]
                ] for i, todo in enumerate(todos)]
            ]
        ]

Real-time Clock

@component
def Clock(ctx):
    import asyncio

    async def update_time():
        while True:
            await asyncio.sleep(1)
            ctx.refresh()

    # Start the update loop
    asyncio.create_task(update_time())

    for _ in ctx:
        current_time = time.strftime("%H:%M:%S")
        yield h.div[
            h.strong["Current time: "],
            current_time
        ]

TypeScript-Style Typing

Crank.py provides comprehensive type safety with TypedDict interfaces, Context typing, and full IDE support through Pyright.

Component Props with TypedDict

Define strict component interfaces using TypedDict:

from typing import TypedDict, Callable, Optional
from crank import component, Context, Props, Children

# Required and optional props
class ButtonProps(TypedDict, total=False):
    onclick: Callable[[], None]  # Event handlers always lowercase
    disabled: bool
    variant: str  # e.g., "primary", "secondary"
    children: Children

# Complex component with nested data
class TodoItemProps(TypedDict):
    todo: "TodoDict"  # Reference to another type
    ontoggle: Callable[[int], None]
    ondelete: Callable[[int], None]
    onedit: Callable[[int, str], None]

class TodoDict(TypedDict):
    id: int
    title: str
    completed: bool

# Type-safe components
@component
def Button(ctx: Context, props: ButtonProps):
    for props in ctx:
        yield h.button(
            onclick=props.get("onclick"),
            disabled=props.get("disabled", False),
            className=f"btn btn-{props.get('variant', 'primary')}"
        )[props.get("children", "Click me")]

@component
def TodoItem(ctx: Context, props: TodoItemProps):
    for props in ctx:
        todo = props["todo"]
        yield h.li[
            h.input(
                type="checkbox",
                checked=todo["completed"],
                onchange=lambda: props["ontoggle"](todo["id"])
            ),
            h.span[todo["title"]],
            h.button(onclick=lambda: props["ondelete"](todo["id"]))["×"]
        ]

Core Crank.py Types

from crank import Element, Context, Props, Children

# Basic types
Props = Dict[str, Any]  # General props dict
Children = Union[str, Element, List["Children"]]  # Nested content

# Generic Context typing (similar to Crank.js)
Context[PropsType, ResultType]  # T = props type, TResult = element result type

# Context with full method typing
def my_component(ctx: Context[MyProps, Element], props: MyProps):
    # All context methods are typed
    ctx.refresh()  # () -> None
    ctx.schedule(callback)  # (Callable) -> None
    ctx.after(callback)    # (Callable) -> None
    ctx.cleanup(callback)  # (Callable) -> None

    # Iterator protocol for generator components
    for props in ctx:  # Each iteration gets updated props (typed as MyProps)
        yield h.div["Updated with new props"]

    # Direct props access with typing
    current_props: MyProps = ctx.props

Component Patterns & Generics

Create reusable, typed component patterns:

from typing import TypedDict, Generic, TypeVar, List

# Generic list component
T = TypeVar('T')

class ListProps(TypedDict, Generic[T]):
    items: List[T]
    render_item: Callable[[T], Element]
    onselect: Callable[[T], None]

@component
def GenericList(ctx: Context[ListProps[T], Element], props: ListProps[T]):
    for props in ctx:  # props is properly typed as ListProps[T]
        yield h.ul[
            [h.li(
                key=i,
                onclick=lambda item=item: props["onselect"](item)
            )[props["render_item"](item)]
             for i, item in enumerate(props["items"])]
        ]

# Usage with type inference
user_list_props: ListProps[User] = {
    "items": users,
    "render_item": lambda user: h.span[user.name],
    "onselect": handle_user_select
}

Advanced Props Patterns

# Union types for polymorphic components
from typing import Union, Literal

class IconButtonProps(TypedDict, total=False):
    variant: Literal["icon", "text", "both"]
    icon: str
    onclick: Callable[[], None]
    children: Children

class FormFieldProps(TypedDict):
    name: str
    value: Union[str, int, bool]
    onchange: Callable[[Union[str, int, bool]], None]
    # Discriminated union based on field type
    field_type: Literal["text", "number", "checkbox"]

@component
def FormField(ctx: Context, props: FormFieldProps):
    for props in ctx:
        field_type = props["field_type"]

        if field_type == "checkbox":
            yield h.input(
                type="checkbox",
                name=props["name"],
                checked=bool(props["value"]),
                onchange=lambda e: props["onchange"](e.target.checked)
            )
        elif field_type == "number":
            yield h.input(
                type="number",
                name=props["name"],
                value=str(props["value"]),
                onchange=lambda e: props["onchange"](int(e.target.value))
            )
        else:  # text
            yield h.input(
                type="text",
                name=props["name"],
                value=str(props["value"]),
                onchange=lambda e: props["onchange"](e.target.value)
            )

Type Checking Setup

Install and configure Pyright for comprehensive type checking:

# Install type checker
uv add --dev pyright

# Run type checking
uv run pyright crank/

# Run all checks (lint + types)
make check

pyproject.toml configuration:

[tool.pyright]
pythonVersion = "3.8"
typeCheckingMode = "basic"
reportUnknownMemberType = false  # For JS interop
reportMissingImports = false     # Ignore PyScript imports
include = ["crank"]
exclude = ["tests", "examples"]

Props as Dictionaries

Components receive props as Python dictionaries (converted from JS objects):

@component
def MyComponent(ctx: Context, props: Props):
    for props in ctx:
        # Access props using dict syntax
        title = props["title"]
        onclick = props["onclick"]

        yield h.div[
            h.h1[title],
            h.button(onclick=onclick)["Click me"]
        ]

Event Props Convention

Use lowercase for all event and callback props:

  • onclick not onClick
  • onchange not onChange
  • ontoggle not onToggle

This matches HTML attribute conventions and provides consistency.

Testing

Run the test suite:

# Install dependencies
pip install pytest playwright

# Run tests
pytest tests/

Development

# Clone the repository
git clone https://github.com/bikeshaving/crankpy.git crankpy
cd crankpy

# Install in development mode
pip install -e ".[dev]"

# Run examples
python -m http.server 8000
# Visit http://localhost:8000/examples/

Why Crank.py?

Python Web Development, Modernized

Traditional Python web frameworks use templates and server-side rendering. Crank.py brings component-based architecture to Python:

  • Reusable Components - Build UIs from composable pieces
  • Dynamic Updates - Explicit re-rendering with ctx.refresh()
  • Generator-Powered - Natural state management with Python generators
  • Browser-Native - Run Python directly in the browser via PyScript

Perfect for:

  • PyScript Applications - Rich client-side Python apps
  • Educational Projects - Teaching web development with Python
  • Prototyping - Rapid UI development without JavaScript
  • Data Visualization - Interactive Python data apps in the browser

Advanced Features

Refs - Direct DOM Access

Use ref callbacks to access rendered DOM elements directly:

@component
def VideoPlayer(ctx):
    video_element = None

    def set_video_ref(el):
        nonlocal video_element
        video_element = el

    @ctx.refresh
    def play():
        if video_element:
            video_element.play()

    @ctx.refresh
    def pause():
        if video_element:
            video_element.pause()

    for _ in ctx:
        yield h.div[
            h.video(
                src="/path/to/video.mp4",
                ref=set_video_ref
            ),
            h.button(onclick=play)["Play"],
            h.button(onclick=pause)["Pause"]
        ]

Ref Patterns:

  • Refs fire once when elements are first rendered
  • Don't work on fragments - use on host elements only
  • For components, explicitly pass ref prop to child elements, or call it yourself
  • Useful for focus management, DOM measurements, third-party integrations
@component
def AutoFocusInput(ctx, props):
    for props in ctx:
        yield h.input(
            type="text",
            placeholder=props.get("placeholder", ""),
            ref=lambda el: el.focus()  # Auto-focus when rendered
        )

Fragments - Multiple Children Without Wrappers

Fragments let you return multiple elements without extra DOM nodes:

# Simple fragments - just use Python lists!
@component
def UserInfo(ctx, props):
    user = props["user"]
    for props in ctx:
        yield [
            h.h2[user["name"]],
            h.p[user["bio"]],
            h.span[f"Joined: {user['joined']}"]
        ]

# Fragment with props (for keys, etc.)
@component
def ConditionalContent(ctx, props):
    show_content = props.get("show", False)
    for props in ctx:
        if show_content:
            yield h("", key="content-fragment")[
                h.div["Content block 1"],
                h.div["Content block 2"]
            ]
        else:
            yield h("", key="empty-fragment")["No content"]

# Mixed fragments in JSX-like syntax
@component
def Navigation(ctx):
    for _ in ctx:
        yield h.nav[
            h.div(className="logo")["MyApp"],
            [  # Fragment for nav items
                h.a(href="/home")["Home"],
                h.a(href="/about")["About"],
                h.a(href="/contact")["Contact"]
            ],
            h.button["Menu"]
        ]

Key Prop - List Reconciliation

Keys help Crank identify which elements have changed in lists:

@component
def TodoList(ctx, props):
    for props in ctx:
        todos = props["todos"]
        yield h.ul[
            [h.li(key=todo["id"])[
                h.input(
                    type="checkbox",
                    checked=todo["completed"],
                    onchange=lambda todo_id=todo["id"]: props["onToggle"](todo_id)
                ),
                h.span[todo["text"]],
                h.button(onclick=lambda todo_id=todo["id"]: props["onDelete"](todo_id))["×"]
            ] for todo in todos]
        ]

# Without keys - elements match by position (can cause issues)
# With keys - elements match by identity (preserves state correctly)

@component
def DynamicList(ctx):
    items = ["A", "B", "C", "D"]
    reversed_items = False

    @ctx.refresh
    def toggle_order():
        nonlocal reversed_items
        reversed_items = not reversed_items

    for _ in ctx:
        current_items = items[::-1] if reversed_items else items
        yield h.div[
            h.button(onclick=toggle_order)["Toggle Order"],
            h.ul[
                [h.li(key=item)[
                    f"Item {item} (with preserved state)"
                ] for item in current_items]
            ]
        ]

Key Guidelines:

  • Use stable, unique values (IDs, not array indices)
  • Keys only need to be unique among siblings
  • Can be strings, numbers, or any JavaScript value
  • Essential for stateful components and form inputs

Copy Prop - Prevent Re-rendering

The copy prop prevents elements from re-rendering for performance optimization:

@component
def ExpensiveList(ctx, props):
    for props in ctx:
        items = props["items"]
        yield h.ul[
            [h.li(
                key=item["id"],
                copy=not item.get("hasChanged", True)  # Skip render if unchanged
            )[
                h(ExpensiveComponent, data=item["data"])
            ] for item in items]
        ]

# Copy with string selectors (Crank 0.7+)
@component
def SmartForm():
    return h.form[
        # Copy all props except value (keeps input uncontrolled)
        h.input(
            copy="!value",
            type="text",
            placeholder="Enter text...",
            name="username"
            value=props["value"]
        ),

        # Copy only specific props
        h.div(
            copy="class id",
            className="form-section",
            id="user-info",
            data_updated=props.get("timestamp")
        )[
            h.label["Username"]
        ],

        # Copy children from previous render
        h.div(copy="children", className="dynamic")[
            # Children preserved from last render
            props["children"]
        ]
    ]

Copy Prop Syntax:

  • copy=True - Prevent all re-rendering
  • copy=False - Normal re-rendering (default)
  • copy="!value" - Copy all props except value
  • copy="class children" - Copy only class and children
  • Cannot mix ! and regular syntax

Special Components

Raw - Inject HTML/DOM Nodes

@component
def MarkdownRenderer(ctx, props):
    for props in ctx:
        # Process markdown to HTML
        markdown_text = props["markdown"]
        html_content = markdown_to_html(markdown_text)  # Your markdown processor

        yield h.div[
            h(Raw, value=html_content)
        ]

# Insert actual DOM nodes
@component
def CanvasChart(ctx, props):
    for props in ctx:
        # Create chart canvas with external library
        canvas_node = create_chart(props["data"])

        yield h.div[
            h.h3["Sales Chart"],
            h(Raw, value=canvas_node)
        ]

Portal - Render Into Different DOM Location

from js import document

@component
def Modal(ctx, props):
    for props in ctx:
        is_open = props.get("isOpen", False)
        if is_open:
            # Render modal into document body instead of current location
            modal_root = document.getElementById("modal-root")
            yield h(Portal, root=modal_root)[
                h.div(className="modal-backdrop", onclick=props["onClose"])[
                    h.div(className="modal-content", onclick=lambda e: e.stopPropagation())[
                        h.div(className="modal-header")[
                            h.h2[props["title"]],
                            h.button(onclick=props["onClose"])["×"]
                        ],
                        h.div(className="modal-body")[
                            props.get("children", [])
                        ]
                    ]
                ]
            ]

# Usage
@component
def App(ctx):
    show_modal = False

    @ctx.refresh
    def open_modal():
        nonlocal show_modal
        show_modal = True

    @ctx.refresh
    def close_modal():
        nonlocal show_modal
        show_modal = False

    for _ in ctx:
        yield h.div[
            h.h1["My App"],
            h.button(onclick=open_modal)["Open Modal"],
            h(Modal,
                isOpen=show_modal,
                title="Example Modal",
                onClose=close_modal
            )["Modal content here!"]
        ]

Text - Explicit Text Node Control

@component
def TextManipulator(ctx):
    text_node = None

    def set_text_ref(text):
        nonlocal text_node
        text_node = text

    @ctx.refresh
    def update_text():
        if text_node:
            text_node.data = "Updated directly!"

    for _ in ctx:
        yield h.div[
            h(Text, value="Original text", ref=set_text_ref),
            h.button(onclick=update_text)["Update Text"]
        ]

Copy - Prevent Subtree Re-rendering

@component
def MemoizedComponent(ctx, props):
    last_props = None

    for props in ctx:
        if last_props and props_equal(props, last_props):
            # Don't re-render if props haven't changed
            yield h(Copy)
        else:
            yield h(ExpensiveComponent, **props)
        last_props = props

def props_equal(a, b):
    """Shallow comparison of props"""
    return (
        set(a.keys()) == set(b.keys()) and
        all(a[key] == b[key] for key in a.keys())
    )

# Higher-order memo component
def memo(Component):
    @component
    def MemoWrapper(ctx, props):
        last_props = None
        yield h(Component, **props)

        for props in ctx:
            if last_props and props_equal(props, last_props):
                yield h(Copy)
            else:
                yield h(Component, **props)
            last_props = props

    return MemoWrapper

# Usage
@memo
@component
def ExpensiveItem(ctx, props):
    for props in ctx:
        # Expensive computation here
        yield h.div[f"Processed: {props['data']}"]

Performance Patterns

# Combining keys, copy, and memoization
@component
def OptimizedList(ctx, props):
    for props in ctx:
        items = props["items"]
        yield h.ul[
            [h.li(
                key=item["id"],
                copy=not item.get("_dirty", False)  # Skip clean items
            )[
                h(MemoizedItem,
                    data=item["data"],
                    onUpdate=props["onItemUpdate"]
                )
            ] for item in items]
        ]

# Selective prop copying for performance
@component
def SmartComponent(ctx, props):
    for props in ctx:
        yield h.div[
            # Only re-renders children, does not touch class style
            h.div(
                className=props["className"],
                style=props["style"],
                copy="class style"
            )[
                props["dynamicContent"]
            ],

            # Expensive chart that rarely changes
            h.div(copy=not props.get("chartDataChanged", False))[
                h(ChartComponent, data=props["chartData"])
            ]
        ]

Runtime Compatibility

Crank.py runs on Pyodide and on MicroPython.

Element construction is pure Python on both runtimes. The h builder makes plain Python nodes. Crank.py transforms these nodes into createElement calls at the render boundary and at component boundaries.

Pyodide

  • All features work: sync components, generators, async functions, and async generators.

MicroPython

  • Sync components and generator components work.
  • Async generators (async def with yield) are not available. MicroPython does not compile them. Use a regular def with yield instead.
  • MicroPython proxies do not expose Symbol.iterator, so Crank.py drives generators through a wrapper that implements the JavaScript iterator protocol.
# Works in both runtimes
@component
def Counter(ctx):
    count = 0

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

    for _ in ctx:
        yield h.div[
            h.h1[f"Count: {count}"],
            h.button(onclick=increment)["+"]
        ]

Learn More

Contributing

Contributions welcome! Please read our Contributing Guide first.

License

MIT © 2025

About

Python Frontend Framework with Async/Generators, Powered by Crank.js

Resources

Contributing

Stars

9 stars

Watchers

1 watching

Forks

Packages

Used by

Contributors

Languages