Standalone Executable¶
Build your PyWry application as a self-contained executable that runs on machines without Python installed. PyWry ships with a PyInstaller hook that handles everything automatically — no .spec file edits or manual --hidden-import flags required.
Quick Start¶
The output in dist/MyApp/ is a fully portable directory you can zip and distribute.
How It Works¶
When you pip install pywry, a PyInstaller hook is registered via the pyinstaller40 entry point. The next time you run pyinstaller, it automatically:
- Bundles data files — frontend HTML/JS/CSS, gzipped AG Grid and Plotly libraries, Tauri configuration (
Tauri.toml), capability manifests, icons, and MCP skill files. - Includes hidden imports — the Tauri subprocess entry point, native extension modules (
.pyd/.so), pytauri plugins, IPC command handlers, and runtime dependencies likeanyioandimportlib_metadata. - Collects native binaries — the
pytauri_wheelshared library for the current platform.
Subprocess Re-entry¶
PyWry runs Tauri in a subprocess. In a normal Python install this subprocess is python -m pywry. In a frozen executable there is no Python interpreter — the bundled .exe is the app.
PyWry solves this transparently:
runtime.start()detects the frozen environment and re-launchessys.executable(your app) withPYWRY_IS_SUBPROCESS=1in the environment.freeze_support(), called automatically when youimport pywry, intercepts the child process on startup and routes it to the Tauri event loop — your application code never runs a second time in the subprocess.
No special code is needed in your app. A minimal freezable application looks like this:
from pywry import PyWry, WindowMode
app = PyWry(mode=WindowMode.SINGLE_WINDOW, title="My App")
app.show("<h1>Hello from a standalone executable!</h1>")
app.block()
Build Options¶
PyInstaller — One-directory (recommended)¶
--onedir is the default and gives the best startup time. The --windowed flag prevents a console window from appearing on Windows.
PyInstaller — One-file¶
One-file builds are simpler to distribute but have slower startup because PyInstaller extracts everything to a temp directory at launch.
Custom icon¶
On macOS use --icon=icon.icns; on Linux, --icon=icon.png.
Nuitka¶
Nuitka compiles Python to C and produces a native binary. The --include-package=pywry flag ensures all data files and submodules are included.
Target Platform Requirements¶
The output executable is native to the build platform. End users need only the OS-level WebView runtime:
| Platform | Requirement |
|---|---|
| Windows 10 (1803+) / 11 | WebView2 — pre-installed |
| macOS 11+ | WKWebView — built-in |
| Linux | libwebkit2gtk-4.1 (apt install libwebkit2gtk-4.1-0) |
No Python installation is required on the target machine.
Example Application¶
"""Minimal PyWry app that can be built as a standalone distributable."""
from pywry import PyWry, WindowMode
app = PyWry(mode=WindowMode.SINGLE_WINDOW, title="Standalone App")
app.show(
"""
<html>
<body style="background:#1e1e1e; color:white; font-family:sans-serif;
display:flex; align-items:center; justify-content:center;
height:100vh; margin:0;">
<div style="text-align:center;">
<h1>Hello from a distributable executable!</h1>
<p style="color:#888;">No Python installation required on the target machine.</p>
</div>
</body>
</html>
"""
)
app.block()
Build it:
Advanced Topics¶
Explicit freeze_support() Call¶
The interception is automatic on import pywry. For extra safety — ensuring no application code runs before interception — you can call freeze_support() at the very top of your entry point:
if __name__ == "__main__":
from pywry import freeze_support
freeze_support()
# ... rest of application ...
This is only necessary if you have expensive top-level initialization that you want to skip in the subprocess.
Debugging Frozen Builds¶
Enable debug logging to see subprocess communication:
Extra Tauri Plugins¶
If your app uses additional Tauri plugins beyond the defaults (dialog, fs), configure them before app.show():
from pywry import PyWry
app = PyWry(title="My App")
app.tauri_plugins = ["dialog", "fs", "notification", "clipboard-manager"]
app.show("<h1>With extra plugins</h1>")
app.block()
The PyInstaller hook automatically collects all pytauri_plugins submodules, so no manual --hidden-import is needed.
Extra Capabilities¶
For Tauri capability permissions beyond the defaults:
Custom .spec File¶
For complex builds you can generate a .spec file and customize it:
Then edit MyApp.spec to add extra data files, change paths, etc. Rebuild with:
The PyWry hook still runs automatically — the .spec file is additive.
Troubleshooting¶
Window doesn't appear¶
- Verify
--windowedwas used (otherwise the subprocess may not get focus). - Run with
PYWRY_DEBUG=1and check stderr for errors. - On Linux, ensure
libwebkit2gtk-4.1is installed.
Missing assets (blank window)¶
If the window opens but shows a blank page, the frontend assets may not be bundled. Verify the dist/ directory contains pywry/frontend/:
# Windows
dir /s dist\MyApp\_internal\pywry\frontend
# Linux / macOS
find dist/MyApp/_internal/pywry/frontend -type f
If empty, ensure pywry is installed (not just editable-linked) so collect_data_files can find the package files.
ModuleNotFoundError: pytauri_wheel¶
This means the native Tauri runtime wasn't bundled. Ensure you installed pywry from a platform wheel (not a pure-Python sdist):
App runs twice (code executes in subprocess)¶
This should never happen with the automatic freeze_support(). If it does, add the explicit call at the very top of your script: