DESKTOP APPS WITHOUT ELECTRON: PYTHON + PYWEBVIEW AS A REAL ALTERNATIVE
Electron has a problem its ecosystem refuses to admit: it consumes between 150MB and 400MB of RAM at idle, requires Node.js as a runtime, and ships a full Chromium instance bundled in every installation. A "simple" desktop app easily weighs 200MB just for the runtime. For projects where the core logic already lives in Python, there is an alternative few know about: pywebview. Same visual result, a fraction of the weight, no Node.js, no npm.
Stack: Python 3.10+ · pywebview 4.4 · HTML/CSS/JS
Reference project: FocOs v1.3 — Windows + Linux
Goal: Eliminate Electron from the stack — UI in HTML/CSS/JS, backend in pure Python, direct bridge without IPC
01. THE PROBLEM IT SOLVES
Electron forces you to duplicate logic: the UI in JavaScript/TypeScript and the backend in Node.js with IPC to communicate with the system. For Python projects, this means either rewriting the entire backend in Node or maintaining two separate processes. pywebview eliminates this duplication: UI is HTML/CSS/JS, backend is pure Python, and the bridge between them is direct.
// Non-technical explanation
Imagine you want a window with a web screen inside. Electron is like constructing an entire building to hold that window — it brings its own bricks, its own water supply, its own power generator. pywebview is like cutting a hole in your existing wall and fitting the window there — it uses what the house (the operating system) already has. Much lighter, much more efficient.
02. THE HONEST COMPARISON
The numbers make the decision. Electron has stronger support for specific edge cases — native notifications, advanced multi-window, DevTools in production. Outside those cases, pywebview wins on everything that matters for an independent project.
| Factor | Electron | pywebview |
|---|---|---|
| Runtime | Node.js + Chromium (~200MB) | WebView2/WebKit native (0MB extra) |
| App size | 150-300MB minimum | 5-20MB (your code only) |
| Idle RAM | 150-400MB | 30-80MB |
| Backend | Node.js required | Pure Python |
| IPC | ipcMain / ipcRenderer | Direct bridge via js_api |
| Web engine | Fixed Chromium | WebView2 (Win) / WebKit (Linux/Mac) |
| Build | electron-builder | PyInstaller / Nuitka |
03. INSTALLATION AND MINIMAL SETUP
Base installation is one line. Additional dependencies depend on the OS — Windows needs the WebView2 backend, Linux needs the system WebKitGTK.
pip install pywebview
# Windows — WebView2 backend:
pip install pywebview[winforms]
# or explicit WebView2:
pip install pywebview[edgechromium]
# Linux — Ubuntu/Debian:
sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0
# Linux — Arch:
sudo pacman -S python-gobject webkit2gtk
# Minimal working app:
import webview
window = webview.create_window(
title = 'My App',
url = 'https://example.com',
width = 1200,
height = 800,
)
webview.start()
# Load local HTML:
from pathlib import Path
url = Path('web/index.html').resolve().as_uri()
window = webview.create_window(title='My App', url=url)
webview.start()
04. FOCOS ARCHITECTURE — REAL STRUCTURE
FocOs separates the project into two clear domains: the Python backend at the root and the complete frontend under web/. The SQLite database and LLM configuration live in data/, never mixed with UI code.
├── main.py # Entry point + FocOsAPI
├── focos_importer.py # Project import module
├── data/ # Database and config
│ ├── focos.db # SQLite — projects, tasks, workspaces
│ ├── llm_config.json # LLM configuration
│ └── projects_config.json
└── web/ # Complete frontend
├── index.html # Main shell
├── css/
│ ├── main.css # Variables and reset
│ ├── layout.css # Grid and structure
│ └── theme.css # Dark-Amber theme
└── js/
├── bridge.js # Python bridge abstraction
├── llm.js # LLM panel
├── editor.js # Monaco Editor with tabs
├── terminal.js # xterm.js terminal
├── browser.js # Embedded browser
└── dashboard.js # Metrics and projects
05. THE FOCOSAPI CLASS — THE COMPLETE BRIDGE
Every method in FocOsAPI is accessible from JavaScript via window.pywebview.api.method_name(). Python types are serialized and deserialized automatically — no message protocol to maintain, no types to map manually.
import webview, sqlite3, json, os
from pathlib import Path
BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / 'data'
DB_PATH = DATA_DIR / 'focos.db'
class FocOsAPI:
# -- SYSTEM ------------------------------------------------
def get_version(self):
return { 'version': '1.3', 'platform': os.name }
def open_folder(self, path: str):
'''Opens a folder in the native OS file explorer'''
import subprocess
if os.name == 'nt':
subprocess.Popen(['explorer', path])
else:
subprocess.Popen(['xdg-open', path])
return { 'ok': True }
# -- FILES -------------------------------------------------
def open_file_dialog(self):
result = window.create_file_dialog(
dialog_type = webview.FileDialog.OPEN,
allow_multiple = False,
)
if result:
return { 'ok': True, 'path': str(result[0]) }
return { 'ok': False }
def read_file(self, path: str):
p = Path(path)
text = p.read_text(encoding='utf-8', errors='replace')
return { 'ok': True, 'text': text }
def write_file(self, path: str, content: str):
Path(path).write_text(content, encoding='utf-8')
return { 'ok': True }
# -- STARTUP -----------------------------------------------
def main():
DATA_DIR.mkdir(parents=True, exist_ok=True)
init_db()
api = FocOsAPI()
index_url = (BASE_DIR / 'web' / 'index.html').resolve().as_uri()
global window
window = webview.create_window(
title = 'FocOs',
url = index_url,
js_api = api,
width = 1400,
height = 900,
min_size = (900, 600),
resizable = True,
background_color = '#0A0A0A',
)
webview.start(on_start, debug=False)
if __name__ == '__main__':
main()
06. DISTRIBUTING THE APP — PYINSTALLER
PyInstaller bundles the Python interpreter, the app, and all assets into a single executable. The result on Windows is a ~15MB .exe — no external runtime, no visible dependencies for the end user.
pip install pyinstaller
# Windows — separator ';'
pyinstaller --onefile --windowed \
--add-data 'web;web' \
--add-data 'data;data' \
--name 'FocOs' \
main.py
# Linux — separator ':'
pyinstaller --onefile --windowed \
--add-data 'web:web' \
--add-data 'data:data' \
--name 'focos' \
main.py
# Output:
dist/
FocOs.exe # Windows -- ~15MB, no external runtime
focos # Linux -- ~12MB
pywebview uses WebView2 on Windows. It comes pre-installed on Windows 11. On Windows 10 it may require additional installation by the end user. Check availability at: developer.microsoft.com/en-us/microsoft-edge/webview2/
07. REAL LIMITATIONS OF PYWEBVIEW
pywebview is not a universal Electron replacement. There are cases where Electron remains the correct choice. Knowing the constraints before committing to the architecture prevents rewrites.
| Limitation | Python workaround |
|---|---|
DevTools only with debug=True |
Enable in development, disable in production build. |
| Multi-window with constraints | Design the app as an SPA with panels — eliminates the need for multiple windows. |
| No native notification API | plyer or notify2 from Python — independent of pywebview. |
| No system tray / dock API | pystray from Python — integrates in the same process. |
-- CONCLUSION
pywebview + Python is the right architecture for any desktop app where the core backend already lives in Python. Dropping Electron reduces distribution size from ~200MB to ~15MB, idle RAM from ~300MB to ~50MB, and stack complexity from 3 layers to 2 direct layers. FocOs proves it is possible to build a premium desktop app — with a code editor, terminal, browser, and LLM panel — without a single line of Node.js.
> SYSTEM_READY > NODE_ONLINE