tkwry
Keep Tkinter — give it the WebView it never had.
Embed a real system WebView (wry) inside your Frame: modern HTML, JS, and IPC in the same layout as your buttons and tabs — one mainloop, no floating overlay.
Alpha — Early preview (see PyPI badge for the current version). APIs and behavior may change without notice. Not recommended for production use yet.
📖 Overview
Tkinter is still a solid GUI shell — it just had no first-class way to host modern web content inside a widget. Overlay-style WebViews drift out of sync when you move, resize, or switch tabs.
tkwry fills that missing piece:
- True child embedding —
build_as_childvia HWND, NSView, or X11 window ID - One event loop — Tk
mainlooponly; no separate app runtime - IPC / RPC / emit — JS↔Python events and request/response without freezing the UI
- Layout-aware — tracks
pack/grid/place, tabs, andPanedWindow
Pre-built abi3 wheels ship for Windows and macOS. Linux is source-only (best-effort by design) — see Platform notes.
🔧 Requirements
- Python 3.10+
- Tkinter (included with most Python builds)
- Building from source (git clone,
pip install git+…, or Linux) — Rust toolchain (stable);pipuses maturin as the build backend - Windows (x86_64, arm64) — WebView2 Runtime (no fallback engine; see Platform notes)
- macOS — 11 (Big Sur)+, arm64 or x86_64; system WKWebView
- Linux — WebKitGTK 4.1 + GTK 3; X11 or XWayland (
$DISPLAY); source build only (see Installation and Platform notes)
📦 Installation
PyPI (recommended — Windows / macOS wheels)
pip install tkwry
From a git clone (source build)
Cloning the repo and installing locally compiles the Rust extension on your machine. You need a Rust toolchain (rustup) and platform runtimes from Requirements above (WebView2 on Windows, etc.). pip pulls in maturin automatically as the build backend.
git clone https://github.com/mashu3/tkwry.git
cd tkwry
pip install -e .
Use this for development and for running the examples from the tree.
Install a git revision with pip (source build)
pip install git+https://github.com/mashu3/tkwry.git
This builds from source (sdist via git), not a pre-built wheel — needs Rust, same as pip install .. Prefer the PyPI wheel on Windows and macOS unless you need unreleased commits.
Linux (source install)
Install system dependencies, then build from source (support posture: Platform notes):
# Debian / Ubuntu
sudo apt install \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libglib2.0-dev
# Runtime (for end users of your app)
# sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0
pip install maturin
git clone https://github.com/mashu3/tkwry.git
cd tkwry
pip install .
GTK events are pumped automatically on a Tk timer while your app runs.
🚀 Usage
Basic WebView
import tkinter as tk
from tkwry import WebView
root = tk.Tk()
root.geometry("900x600")
frame = tk.Frame(root, bg="#222")
frame.pack(fill="both", expand=True, padx=8, pady=8)
web = WebView(frame, url="https://github.com")
root.mainloop()
IPC and RPC (JavaScript ↔ Python)
Use IPC for fire-and-forget events and RPC for request/response:
| Direction | Role | Python | JavaScript |
|---|---|---|---|
| JS → Python | IPC (event) | set_ipc_handler / ipc_handler= |
window.ipc.postMessage(str) |
| JS → Python | RPC (call) | @web.expose |
await window.tkwry.call(name, ...) |
| Python → JS | Emit (event) | web.emit(event, data) |
window.tkwry.on(event, handler) |
def on_message(msg: str) -> None:
print("from JS:", msg)
web = WebView(
frame,
html='<button onclick="window.ipc.postMessage(\'hi\')">send</button>',
ipc_handler=on_message,
)
Local app assets (app= / tkwry://)
Serve a directory of HTML/CSS/JS through a custom protocol — no localhost HTTP server. Relative links resolve offline (React/Vue/Svelte/Monaco bundles, etc.).
web/
├── index.html
├── style.css
└── assets/
└── main.js
web = WebView(frame, app="./web") # loads tkwry://localhost/index.html
# or: WebView(frame, app="./web/index.html")
# SPA client routes: spa_fallback=True
# Dev: app_dev=True (Cache-Control: no-store) + web.watch_app() for reload
Constructor app= fixes the filesystem root at create time. Later
load_url("tkwry://localhost/other.html") can navigate within that root
(Windows WebView2 rewrites this to https://tkwry.localhost/... internally).
Monaco / CDN scripts may still be loaded from the network inside that HTML when
you choose not to vendor them yet. The Plotly demo toggles CDN vs Local
(app=); Local caches plotly.js under examples/.vendor/.
RPC (expose / window.tkwry.call)
Keep raw ipc_handler + window.ipc.postMessage for free-form events.
On top, expose callables and await them from JS:
web = WebView(frame, html=HTML)
@web.expose
def read_file(path: str) -> str:
return Path(path).read_text(encoding="utf-8")
# Heavy I/O / CPU — run off the Tk thread so the UI stays responsive
@web.expose(thread=True, timeout=30.0)
def heavy_task(data: dict) -> dict:
...
return result
const text = await window.tkwry.call("read_file", path);
// optional JS-side timeout (ms) and Python kwargs:
await window.tkwry.call("heavy_task", payload, {
timeout: 5000,
kwargs: { verbose: true },
});
Execution model: default handlers run on the Tk main thread (safe for
Tk APIs; long work blocks the UI). Pass thread=True / run_in="worker"
to use a background pool. Handlers may also return a
concurrent.futures.Future. Errors reject the Promise with a structured
payload (error.name / error.message; set rpc_traceback=True or
TKWRY_RPC_TRACEBACK=1 for tracebacks). Duplicate method names raise unless
replace=True. Destroy rejects in-flight RPCs. Keyword args go in
{ kwargs: { … } } (a trailing { timeout: ms } is still call options,
not a positional dict). Worker cancel is cooperative. RPC has its own
2048-deep queue so IPC overflow cannot drop tkwry.call.
Python → JS events (emit)
web.emit("data_updated", {"n": 1})
window.tkwry.on("data_updated", (payload) => { ... });
See examples/ipc_demo.py.
Shared session (WebSession)
Share cookies / cache / localStorage across WebViews via wry's
WebContext:
from tkwry import WebSession, WebView
session = WebSession(data_directory="~/.myapp/webview")
left = WebView(frame_a, html=HTML, session=session)
right = WebView(frame_b, html=HTML, session=session)
Convenience: WebView(..., data_directory=...) or ephemeral=True
creates an owned session. Keep the WebSession alive while any WebView
uses it (especially with app= on macOS). On Linux, WebViews that share a
session must use the same app= root. See
examples/browser_demo.py.
Load HTML / evaluate JavaScript
web.load_html("<h1>Hello</h1>")
web.eval_js("document.title = 'Hi'") # fire-and-forget (Tk idle, no return value)
web.eval_js("bad()", on_error=lambda exc: print("eval failed:", exc))
web.eval_js_with_callback("document.title", print) # async; callback on Tk main thread
web.load_url("https://example.com")
web.reload()
print(web.url)
web.focus()
DevTools need devtools=True at construction, then open_devtools() (calling open_devtools() alone is a no-op on macOS if the flag was false).
web = WebView(frame, html="<h1>Hello</h1>", devtools=True)
web.open_devtools()
Rapid load_url / load_html calls are coalesced (last-wins) — load(A); load(B); load(C) loads C only.
eval_js does not return a result (not synchronous). Use eval_js_with_callback when you need the JavaScript return value as a str. Pass on_error= to handle evaluation failures on the Tk main thread; otherwise the traceback is printed to stderr (EvalErrorHandler).
Layout / resize
Bounds sync runs automatically on <Configure>, <Map>, and <Unmap>. Call sync_bounds() manually after custom layout changes so the WebView reflows (e.g. centered images):
web.sync_bounds()
Size contract: once the host is laid out, the mapped Frame.winfo_width() / winfo_height() are the sole source of truth for native bounds. Constructor width/height and explicit place(..., width=, height=) are only used before Tk reports a real size (winfo_* <= 1). Prefer passing width/height to place() so the host gets a definite allocation (especially on Linux / Xvfb).
Unmapped hosts (inactive Notebook tabs) call set_visible(False). ready stays layout-based (True while hidden); use phase is WebViewPhase.HIDDEN when you need visibility.
Navigation / lifecycle callbacks
from tkwry import NewWindowResponse, PageLoadEvent
web = WebView(
frame,
url="https://example.com",
on_page_load=lambda evt, url: print(evt, url),
on_title_changed=lambda title: root.title(title),
on_navigation=lambda url: url.startswith("https://"),
on_new_window=lambda url: NewWindowResponse.Deny,
)
on_page_load fires PageLoadEvent.Started and PageLoadEvent.Finished for every navigation while a handler is registered (native listening follows the handler). Events are not replayed for navigations that happened before set_on_page_load / constructor on_page_load.
Callback threads: lifecycle / IPC / page-load / title / DnD handlers run on
the Tk main thread. RPC handlers default to the same thread; use
@web.expose(thread=True) for background work. on_navigation and
on_new_window are also invoked on Tk, but WebKit blocks until they return
a value — keep them fast (heavy work → return deny/default and defer with
root.after). Do not create another WebView from on_new_window (even
deferred): WKWebView deadlocks. Intercept links in JS instead (see
examples/browser_demo.py). Timed-out sync hooks
are canceled after about 60s total wait.
Async queues (IPC, RPC, page-load, title, drag-drop, eval) cap at 2048 pending items each; further events are compacted or dropped. RPC is a separate queue from IPC. Use take_queue_drop_counts() to observe overflows — it returns (ipc, page_load, title, drag_drop, eval, rpc).
Callback exceptions are printed to stderr and do not stop event delivery.
Drag & drop (native OS path)
File drops from Finder / Explorer are handled by the OS WebView. Your handler runs on the Tk main thread (tkwry queues events from WebKit automatically). The handler is notify-only (-> None); drops are always accepted and cannot be denied from Python.
from tkwry import DragDropEvent
def on_drop(event, paths, position):
if event == DragDropEvent.Drop:
print("files:", paths)
web = WebView(frame, html="...", drag_drop_handler=on_drop)
See examples/dnd_demo.py.
Cleanup
web.destroy() # release native webview; host Frame is kept
# or destroy the host Frame — both tear down the webview
📚 API summary
| Category | Members |
|---|---|
| Content | load_url, load_html, reload, url |
| JavaScript | eval_js (on_error), eval_js_with_callback |
| IPC / RPC / emit | set_ipc_handler, expose / unexpose, emit, watch_app |
| Callbacks | set_on_navigation, set_on_page_load, set_on_title_changed, set_on_new_window, set_drag_drop_handler |
| Appearance | set_background_color, focus, focus_parent, open_devtools, close_devtools, is_devtools_open |
| Create-only | set_user_agent, set_initialization_script (raise after native create) |
| Layout | pack, grid, place, sync_bounds (delegate to host Frame except sync_bounds) |
| Lifecycle | ready, phase / WebViewPhase, when_ready, wait_until_ready, bind, destroy, destroyed, native, creation_failed, creation_error |
| Diagnostics | take_queue_drop_counts |
Constructor options: width / height, url, html, app, spa_fallback,
app_dev, session / data_directory / ephemeral, ipc_handler,
rpc_traceback, devtools, background_color, user_agent,
initialization_script, focused, plus the callback hooks above.
Enums: PageLoadEvent, NewWindowResponse, DragDropEvent, WebViewPhase.
Exceptions: WebViewNotReadyError, WebViewCreationError, WebViewDestroyedError,
RpcTimeoutError.
Type aliases: IpcHandler, NavigationHandler, PageLoadHandler, TitleChangedHandler, NewWindowHandler, DragDropHandler, EvalCallback, EvalErrorHandler.
⚠️ Known limitations
Short checklist — details live in Platform notes (especially macOS embedding).
- Alpha — APIs may change; not for production yet (see banner above)
- Windows — WebView2 Runtime required; missing runtime →
WebViewCreationError - Windows DevTools — wry/WebView2 reports
is_devtools_open()asFalseandclose_devtools()is a no-op;open_devtools()still opens the inspector - Linux — no PyPI wheel (by design); best-effort source install
- Linux concurrent
eval_js_with_callback— evaluating on multiple WebViews at once can stall WebKitGTK; prefer sequential evals (see Linux) - Linux shared
WebSession+app=— WebViews that share a session must use the sameapp=root (custom protocol is registered once per context) - macOS DevTools — create with
devtools=True, thenopen_devtools()(flag alone does not open;open_devtools()without the flag is a no-op on macOS); uses private APIs — avoid in Mac App Store builds - macOS IME / focus — not Safari-parity; mid-composition focus flips can mis-route input
- macOS import order — import
tkwrybefore AppKit/NSApplication, or you may see a double titlebar url()on macOS — may beNonefor inline HTML until a concreteload_url(WKWebView has no documentNSURL)- Sync hooks / queues —
on_navigation/on_new_windowmay block WebKit up to ~60s; do not create a WebView fromon_new_window; async event queues cap at 2048 (see Navigation / lifecycle callbacks) - Drag & drop — WebView area only (use tkinterdnd2 for arbitrary Tk widgets)
See CHANGELOG.md for release history.
🌐 Platform notes
| OS | Arch | Parent handle | Engine |
|---|---|---|---|
| Windows | x86_64, arm64 | Frame.winfo_id() → HWND |
WebView2 |
| macOS | arm64, x86_64 | Toplevel content NSView |
WKWebView |
| Linux | — | winfo_id() → X11 window ID |
WebKitGTK |
Windows
WebView2 Runtime must be installed (common on Windows 10/11). Without it, creation fails with WebViewCreationError (install link in the message). There is no fallback engine.
DPI: set_bounds uses physical pixels on Windows. After process DPI awareness (e.g. tkface.win.enable_dpi_awareness() before tk.Tk()), Tk winfo_* already reports physical sizes — passing them as wry Logical would double-scale. Prefer awareness + design-pixel→physical sizing in the host app; do not monkeypatch tkwry bounds from app code.
Linux
By design in v0.1.x: no PyPI wheel; install from source (sdist / git). Support is best-effort — not a release blocker for Windows/macOS wheels. CI runs the integration suite under Xvfb; real-desktop / Wayland timing may still differ. GTK is pumped on a Tk timer automatically after install.
For place layouts, pass explicit width/height so host winfo_* settles; native size follows those winfo_* values (see Layout / resize).
Concurrent eval: calling eval_js_with_callback on multiple WebViews at the same time can stall under WebKitGTK (especially headless / Xvfb). Prefer one eval at a time — wait for each callback (or error) before starting the next — when you have several views.
macOS embedding
Tk child Frames usually do not get their own NSView (Tk Aqua). tkwry attaches to the toplevel content view, positions with set_bounds on <Configure>, and hides with set_visible(False) on <Unmap> (e.g. another Notebook tab). Per-frame native views would need upstream Tk changes.
Keyboard focus: clicks are hit-tested at the NSEvent layer; Python drains focus signals on the Tk main thread. Use web.focus() / web.focus_parent() for explicit control (examples/browser_demo.py). On macOS/Windows, focused=True waits for <<WebViewReady>>, then calls focus() (create-time focus breaks child WKWebView / WebView2). Call focus() yourself after later layout changes.
IME: composition stays with the current first responder. Switching Tk ↔ WebView mid-composition (or fighting the system candidate window) can cancel or mis-deliver input vs Safari. Not a v0.1 goal — finish composition before changing focus, or keep IME editing in one surface.
Import order / double titlebar: import tkwry before anything that starts AppKit / NSApplication. On import, tkwry disables process-level automatic window tabbing on the main thread. If AppKit starts first, macOS may show a double titlebar strip. If per-window tabbing disable during create fails, tkwry logs and retries asynchronously (non-fatal).
url(): may be None for inline HTML (html= / load_html) or when WKWebView has no document NSURL. After load_url, it becomes the concrete URI.
Notebook / tabs: unmapped tabs hide the native view (set_visible(False)) and show again on <Map> — required because frames share the toplevel NSView. ready is layout-based (can stay True while hidden); prefer visible-tab work after the tab is selected. No extra app code for tabs/panes — examples/multi_demo.py.
Lifecycle / IPC / page-load handlers run on the Tk main thread. RPC may use a worker (thread=True). on_navigation / on_new_window still make WebKit wait for a return value — see Navigation / lifecycle callbacks.
💡 Why child-window embedding?
Tkinter apps already have a window and a layout. The web belongs inside a Frame — same mainloop, same tabs and panes — not in a separate top-level webview that floats beside your UI. tkwry wraps wry's build_as_child against the native surface Tk gives your widgets.
🧩 Features
- Local app assets —
app=+tkwry://(SPA fallback,app_devno-store,watch_app()hot reload) - IPC / RPC / emit — events vs request/response; worker RPC; structured errors; Python→JS
emit - WebSession — shared wry
WebContext(cookies / cache / localStorage) across WebViews - Testing helpers —
tkwry.testing.wait_until/wait_ready/wait_eval/wait_title - Child-window embedding — WebView is a native child of your Tk window surface, not a floating overlay
- Bounds & visibility sync — follows
<Configure>,<Map>, and<Unmap>(tabs /Notebookhide unmapped views) - Deferred callbacks — IPC, RPC, page load, title, eval results, and DnD queue to Tk (avoids macOS deadlocks)
- URL safety — normalizes and validates URLs before navigation
- DevTools —
devtools=Trueat create, thenopen_devtools()/close_devtools()/is_devtools_open()(macOS: private APIs) - Native drag & drop — OS-level file drops into the WebView (no tkinterdnd2)
- Navigation hooks — all handlers on the Tk thread;
on_navigation/on_new_windowblock WebKit until they return - Multiple layouts — works with
pack,grid,place,Notebook, andPanedWindow(see examples) - Plotly-ready — load HTML +
eval_js; demo toggles CDN vs localapp= - Folium-ready — embed Leaflet maps from Folium HTML (right-click to pin)
- Markdown-ready — Monaco editor + live preview in a
PanedWindow(seeexamples/markdown_demo.py; CDN required — or vendor underapp=) - CI-tested —
pyteston Windows (x86_64 + arm64), macOS, and Linux (Xvfb + WebKitGTK)
📁 Examples
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
| Script | Description |
|---|---|
examples/browser_demo.py |
URL bar, tabs, shared WebSession, open-in-new-tab |
examples/ipc_demo.py |
IPC events, RPC (call / kwargs / worker), and emit |
examples/multi_demo.py |
Multiple WebViews, tabs, panes |
examples/plotly_demo.py |
Plotly charts — CDN or local app= (pip install plotly) |
examples/folium_demo.py |
Folium maps (pip install folium; tiles need the network) |
examples/markdown_demo.py |
Monaco markdown editor + live preview (CDN) |
examples/dnd_demo.py |
Native file drag & drop into WebView |
python examples/browser_demo.py
python examples/ipc_demo.py
python examples/multi_demo.py
python examples/plotly_demo.py
python examples/folium_demo.py
python examples/markdown_demo.py
python examples/dnd_demo.py
📝 License
This project is licensed under the MIT License. See LICENSE.
This project links against wry, which is dual-licensed (Apache-2.0 or MIT). tkwry uses wry under MIT; see NOTICE for attribution.
👨💻 Author
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tkwry-0.1.2.tar.gz.
File metadata
- Download URL: tkwry-0.1.2.tar.gz
- Upload date:
- Size: 206.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a08ffec6b87f682f68deaab28c36d94f7c6c033a792bcc54278934d52a52cda
|
|
| MD5 |
3cb2268d16391b6a17a11dc7aaae0371
|
|
| BLAKE2b-256 |
2a097464410f860703d064a3780d1ac25ac7de8eed1d63de2a38536927f1d1e8
|
File details
Details for the file tkwry-0.1.2-cp310-abi3-win_arm64.whl.
File metadata
- Download URL: tkwry-0.1.2-cp310-abi3-win_arm64.whl
- Upload date:
- Size: 430.6 kB
- Tags: CPython 3.10+, Windows ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6da5e07833596205825732d2c0e8cc038a4ac38c4dae7f3f2d3abb8b22d40f56
|
|
| MD5 |
5f22ef2cad6b9ca06af478cb9453707c
|
|
| BLAKE2b-256 |
97507fc0105013230a2baeaf6e0f6f91b52f434d9cdc389ef7e6d842ca204b4b
|
File details
Details for the file tkwry-0.1.2-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: tkwry-0.1.2-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 444.4 kB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8ebda68d8ff2f3eb1643d74c18dd4ad697a56687afc3cc10f6e06dd8f599e82
|
|
| MD5 |
2bc8fd5d66ece238dc11d681e3ca9a5c
|
|
| BLAKE2b-256 |
29f35d7315bdee00b676d34ec536979ac9df1a0a6ff3b424fa7f7c3bda1ea610
|
File details
Details for the file tkwry-0.1.2-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: tkwry-0.1.2-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 514.8 kB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6bff8f7b37ab6dc436028dcae3e83e9b179a2050e7f372444545eff4b546ec7
|
|
| MD5 |
2ffe833f78a5c7e8f186baa960da4a72
|
|
| BLAKE2b-256 |
7133cabd18ae3a0aa824bd31df7729e4facf38aeb43d36c5434ca40c0f498ca9
|
File details
Details for the file tkwry-0.1.2-cp310-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: tkwry-0.1.2-cp310-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 536.3 kB
- Tags: CPython 3.10+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7d10cd33f2cbce9ca9f19af968e57e7421714ddee79088e5f7dd86a038334d6
|
|
| MD5 |
fe8652e22578babeab91dc43facfefda
|
|
| BLAKE2b-256 |
0119252088fd0b0476af51433eb8f6d894513ca979f9155615bc081045795a4b
|