Anywidget & Widget Protocol¶
PyWry supports three rendering paths — native desktop windows, anywidget-based Jupyter widgets, and IFrame + FastAPI server. All three implement the same BaseWidget protocol, so your application code works identically regardless of environment.
For the protocol and widget API reference, see BaseWidget, PyWryWidget, and InlineWidget.
Rendering Path Auto-Detection¶
PyWry.show() automatically selects the best rendering path:
Script / Terminal ──→ Native OS window (PyTauri subprocess)
Notebook + anywidget ──→ PyWryWidget (traitlet sync, no server)
Notebook + Plotly/Grid ──→ InlineWidget (FastAPI + IFrame)
Notebook without anywidget ──→ InlineWidget (FastAPI fallback)
Browser / SSH / headless ──→ InlineWidget (opens system browser)
No configuration needed — the right path is chosen at show() time.
The BaseWidget Protocol¶
Every rendering backend implements this protocol:
from pywry.widget_protocol import BaseWidget
def use_widget(widget: BaseWidget):
# Register a JS → Python event handler
widget.on("app:click", lambda data, event_type, label: print(data))
# Send a Python → JS event
widget.emit("app:update", {"key": "value"})
# Replace the widget HTML
widget.update("<h1>New Content</h1>")
# Show the widget in the current context
widget.display()
| Method | Description |
|---|---|
on(event_type, callback) |
Register callback for JS → Python events. Callback receives (data, event_type, label). Returns self for chaining. |
emit(event_type, data) |
Send Python → JS event with a JSON-serializable payload. |
update(html) |
Replace the widget's HTML content. |
display() |
Show the widget (native window, notebook cell, or browser tab). |
Level 1: PyWryWidget (anywidget)¶
The best notebook experience. Uses anywidget's traitlet sync — no server needed, instant bidirectional communication through the Jupyter kernel.
Requirements: pip install anywidget traitlets
from pywry import PyWry
app = PyWry()
widget = app.show("<h1>Hello from anywidget!</h1>")
# Events work identically
widget.on("app:ready", lambda d, e, l: print("Widget ready"))
widget.emit("app:update", {"count": 42})
How it works:
- Python creates a
PyWryWidget(extendsanywidget.AnyWidget) - An ESM module is bundled as the widget frontend
- Traitlets (
content,theme,_js_event,_py_event) sync bidirectionally via Jupyter comms widget.emit()→ sets_py_eventtraitlet → JS receives change → dispatches to JS listeners- JS
pywry.emit()→ sets_js_eventtraitlet → Python_handle_js_event()→ dispatches to callbacks
When it's used: Notebook environment + anywidget installed + no Plotly/AG Grid/TradingView content.
Level 2: InlineWidget (IFrame + FastAPI)¶
Used for Plotly, AG Grid, and TradingView content in notebooks, or when anywidget isn't installed. Starts a local FastAPI server and renders via an IFrame.
Requirements: pip install fastapi uvicorn
from pywry import PyWry
app = PyWry()
# Plotly/Grid/TradingView automatically use InlineWidget
handle = app.show_plotly(fig)
handle = app.show_dataframe(df)
handle = app.show_tvchart(ohlcv_data)
How it works:
- A singleton FastAPI server starts in a background thread (one per kernel)
- Each widget gets a URL (
/widget/{widget_id}) and a WebSocket (/ws/{widget_id}) - An IFrame in the notebook cell points to the widget URL
widget.emit()→ enqueues event → WebSocket send loop pushes to browser- JS
pywry.emit()→ sends over WebSocket → FastAPI handler dispatches to Python callbacks
Multiple widgets share one server — efficient for dashboards with many components.
Browser-only mode:
widget = app.show("<h1>Dashboard</h1>")
widget.open_in_browser() # Opens system browser instead of notebook
Level 3: NativeWindowHandle (Desktop)¶
Used in scripts and terminals. The PyTauri subprocess manages native OS webview windows.
from pywry import PyWry
app = PyWry(title="My App", width=800, height=600)
handle = app.show("<h1>Native Window</h1>")
# Same API as notebook widgets
handle.on("app:click", lambda d, e, l: print("Clicked!", d))
handle.emit("app:update", {"status": "ready"})
# Additional native-only features
handle.close()
handle.hide()
handle.eval_js("document.title = 'Updated'")
print(handle.label) # Window label
How it works: JSON-over-stdin/stdout IPC to the PyTauri Rust subprocess. The subprocess manages the OS webview (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux).
Writing Portable Code¶
Since all three backends share the BaseWidget protocol, write code against the protocol:
def setup_dashboard(widget):
"""Works with any widget type."""
widget.on("app:ready", lambda d, e, l: print("Ready in", l))
widget.on("app:click", handle_click)
widget.emit("app:config", {"theme": "dark"})
# Works everywhere
handle = app.show(my_html)
setup_dashboard(handle)
Fallback Behavior¶
If anywidget is not installed, PyWryWidget becomes a stub that shows an error message with install instructions. The InlineWidget fallback handles all notebook rendering in that case.
| Scenario | Widget Used | Fallback |
|---|---|---|
| Notebook + anywidget + simple HTML | PyWryWidget |
— |
| Notebook + anywidget + Plotly | InlineWidget |
— |
| Notebook, no anywidget | InlineWidget |
IFrame server |
| Desktop script | NativeWindowHandle |
— |
| SSH / headless | InlineWidget |
Opens browser |