Language IntegrationsPython

Python #

The microsoft-webui package provides high-performance server-side rendering for Python. It is a direct PyO3 native extension โ€” not a ctypes wrapper around the C API โ€” imported as microsoft_webui. Rendering calls release the GIL, so a Renderer shared across threads keeps rendering while the rest of your application runs Python code.

Installation #

The microsoft-webui package is not published to PyPI yet. Wheels and a source distribution are built and attached to each GitHub Release; install one directly, or build from a checkout with maturin.

# From a GitHub Release asset
pip install ./microsoft_webui-<version>-cp311-abi3-<platform>.whl

# Or from a checkout
pip install maturin
maturin build --release --manifest-path crates/webui-python/Cargo.toml

The package ships prebuilt wheels for CPython 3.11+ on Windows, macOS, and Linux (manylinux), each for x86_64 and ARM64, plus one source distribution. See Wheel matrix and scope for what isn't covered and why.

Buffered rendering #

Load protocol.bin once and reuse the Renderer for the process lifetime:

from microsoft_webui import Renderer

renderer = Renderer.from_file("dist/protocol.bin", plugin="webui")

html = renderer.render(
    {"title": "Home"},
    entry="index.html",
    request_path="/",
)  # -> bytes

render() returns bytes, the canonical fast path for writing directly to a socket or WSGI/ASGI response. Use render_text() when you need a str:

html_str = renderer.render_text({"title": "Home"}, request_path="/")

WSGI #

from wsgiref.simple_server import make_server
from microsoft_webui import Renderer

renderer = Renderer.from_file("dist/protocol.bin", plugin="webui")

def app(environ, start_response):
    html = renderer.render(
        {"title": "Home"},
        request_path=environ.get("PATH_INFO", "/"),
    )
    start_response("200 OK", [
        ("Content-Type", "text/html; charset=utf-8"),
        ("Content-Length", str(len(html))),
    ])
    return [html]

if __name__ == "__main__":
    with make_server("", 8000, app) as httpd:
        httpd.serve_forever()

ASGI (Starlette / FastAPI) #

renderer.render() is synchronous and CPU-bound. It releases the GIL inside Rust, but it still blocks the calling Python thread, so an ASGI app should offload it rather than call it straight from the event loop:

import anyio  # or asyncio.to_thread on stdlib-only setups
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Route
from microsoft_webui import Renderer

renderer = Renderer.from_file("dist/protocol.bin", plugin="webui")

async def index(request):
    html = await anyio.to_thread.run_sync(
        lambda: renderer.render({"title": "Home"}, request_path=request.url.path),
    )
    return Response(html, media_type="text/html; charset=utf-8")

app = Starlette(routes=[Route("/{path:path}", index)])

Renderer is thread-safe, so many worker threads can render through the same instance concurrently โ€” offloading to a thread pool doesn't introduce a race.

Partial navigation and component templates #

partial = renderer.render_partial(
    {"title": "Home"},
    entry="index.html",
    request_path="/users/42",
    inventory=client_inventory_hex,
)  # -> bytes, complete JSON partial-navigation response

templates = renderer.render_component_templates(
    ["user-card", "avatar"],
    inventory=client_inventory_hex,
)  # -> bytes, requested component template payloads

Both return bytes; write them directly as the response body for Router.ensureLoaded() / partial-navigation requests from the client.

Progressive (host-driven) streaming #

renderer.stream_response() opens a StreamingSession whose methods return the bytes they produced instead of writing anywhere themselves. WebUI never touches your socket, so your server owns the write order and backpressure - the same contract as every other host binding (see Streaming Boundaries for the authoring side and full ordering rules).

WSGI #

WSGI's response is already an iterable of bytes chunks, so a generator maps directly onto the session:

def app(environ, start_response):
    session = renderer.stream_response(request_path=environ.get("PATH_INFO", "/"))

    def body():
        step = session.start(initial_state)
        yield step.bytes
        while not step.done:
            boundary = step.boundary
            if boundary is not None:
                state = load_boundary_state(
                    boundary.owner,
                    boundary.name,
                    boundary.key,
                )
                step = session.resume(
                    boundary.instance_id,
                    state,
                    mode="final",
                )
            else:
                step = session.advance()
            yield step.bytes

    start_response("200 OK", [
        ("Content-Type", "text/html; charset=utf-8"),
        ("X-Accel-Buffering", "no"),
    ])
    return body()

ASGI (Starlette / FastAPI) #

Each session call is still synchronous and GIL-releasing, not async, so offload it the same way as buffered rendering:

import anyio
from starlette.responses import StreamingResponse

async def index(request):
    session = renderer.stream_response(request_path=request.url.path)

    async def body():
        step = await anyio.to_thread.run_sync(session.start, initial_state)
        yield step.bytes
        while not step.done:
            boundary = step.boundary
            if boundary is not None:
                state = await load_boundary_state(
                    boundary.owner,
                    boundary.name,
                    boundary.key,
                )
                step = await anyio.to_thread.run_sync(
                    lambda: session.resume(
                        boundary.instance_id,
                        state,
                        mode="final",
                    ),
                )
            else:
                step = await anyio.to_thread.run_sync(session.advance)
            yield step.bytes

    return StreamingResponse(body(), media_type="text/html; charset=utf-8")

start(), resume(), and advance() return a StreamStep with bytes, done, and an optional descriptor. The descriptor provides instance_id, declaration_id, owner, name, and key. A descriptor means call resume(); no descriptor with done == False means call advance(); a true done value means complete.

resume() returns only the pending occurrence through its checkpoint. advance() returns the following parent or tail bytes through the next descriptor or terminal. This separation lets the host yield the checkpoint immediately, with no sibling boundary workaround. update() is valid between the occurrence's resume() and advance(). The completed step already includes the tail and terminal bytes.

Sessions are not multi-driver - drive one session from one thread at a time; independent sessions on the same Renderer may run concurrently.

API reference #

PythonDescription
Renderer(protocol_bytes, *, plugin=None)Decode and index protocol bytes once, binding an optional named plugin
Renderer.from_file(path, *, plugin=None)Read path and construct a Renderer from its bytes
renderer.render(state, *, entry="index.html", request_path="/")Render into bytes
renderer.render_text(state, *, entry="index.html", request_path="/")Render and decode into str
renderer.render_partial(state, *, entry="index.html", request_path="/", inventory="")Complete JSON partial-navigation response as bytes
renderer.render_component_templates(tags, *, inventory="")On-demand component template payloads as bytes
renderer.tokenstuple[str, ...] of CSS token names in build order
renderer.stream_response(*, entry="index.html", request_path="/", nonce=None, head_inject=None, body_inject=None)Open a host-driven StreamingSession

StreamingSession #

MemberDescription
start(state) -> StreamStepBytes through the first runtime occurrence or terminal
resume(instance_id, state, mode=BoundaryMode.FINAL) -> StreamStepBytes for only the pending occurrence through its checkpoint
advance() -> StreamStepFollowing parent bytes through the next occurrence or terminal
update(instance_id, patch) -> bytesProjected state for a committed updatable occurrence

Plugin and BoundaryMode #

Both are enum.StrEnum (standard in CPython 3.11+, this package's minimum interpreter version), so they compare equal to plain strings while staying typo-checked by static analysis:

from microsoft_webui import BoundaryMode

session.resume(boundary.instance_id, state, mode=BoundaryMode.UPDATABLE)
session.resume(boundary.instance_id, state, mode="updatable")  # equivalent

Fast path: pre-serialized state #

Every state parameter accepts a Python Mapping, which the facade serializes with the standard library json module before crossing into Rust. If you already have JSON โ€” cached, streamed from another service, or assembled with a faster serializer โ€” pass str, bytes, bytearray, or a memoryview directly. Every pre-serialized form bypasses json.dumps; immutable str and bytes remain backed by their Python objects while Rust renders with the GIL released, while mutable/general buffers are copied first for safety:

import json

renderer.render({"title": "Home"})               # facade calls json.dumps for you
renderer.render(json.dumps({"title": "Home"}))    # str fast path
renderer.render(b'{"title":"Home"}')              # bytes fast path

Head/body injection #

head_inject, body_inject, and the reserved $webui state channel's headEnd / bodyStart / bodyEnd members are written verbatim at their structural boundary, exactly like every other host binding.

These fields are not escaped. Passing user-controlled content here is a direct cross-site scripting (XSS) vector. Only pass fully trusted content (build-time-derived markup, a dev livereload script) or content you have already escaped yourself.

Wheel matrix and scope #

microsoft-webui builds against PyO3's abi3-py311 stable ABI, so one wheel per platform serves every CPython 3.11+ interpreter โ€” no per-minor-version build matrix. v1 ships six wheels (Windows, macOS, and manylinux, each for x86_64 and ARM64; macOS ships separate x86_64/ARM64 wheels, not a universal2 fat binary) plus one sdist.

v1 is runtime-only: it renders compiled protocols and does not expose a build/compile API. Produce protocol.bin with webui build (the npm or Rust CLI) โ€” Python consumes the compiled artifact like every other host.

Not covered in v1: PyPy, GraalPy, free-threaded CPython builds, musllinux, and 32-bit architectures. The self-contained sdist bundles the matching WebUI Rust source closure and lets a Rust toolchain build any of those targets, but doing so is unsupported and untested. If you're on one of those platforms today, the C API's ctypes fallback can still reach WebUI directly.

Next Steps #