A modern desktop application template combining pywebview (Python) with SvelteKit (TypeScript). Build native desktop apps with a Python backend and a reactive Svelte frontend.
| Layer | Technology |
|---|---|
| Backend | Python 3.12+, pywebview 5.0+ |
| Frontend | SvelteKit 2, Svelte 5, TypeScript |
| Tooling | uv, Vite, ESLint, Prettier, Ruff, mypy |
# Clone the repository
git clone https://github.com/yourusername/svelte-python-desktop.git
cd svelte-python-desktop
# Install Python dependencies
uv sync
# Install frontend dependencies
cd frontend && npm install && cd ..
Start the app with hot reload enabled for both Python and frontend:
uv run python -m jurigged -v scripts/dev.py
This command:
localhost:5173# Build the frontend
cd frontend && npm run build && cd ..
# Run the production app
uv run python -m backend.main
svelte-python-desktop/
├── backend/ # Python backend
│ ├── __init__.py
│ ├── main.py # App entry point, window configuration
│ └── api.py # API methods exposed to JavaScript
├── frontend/ # SvelteKit frontend
│ ├── src/
│ │ ├── lib/
│ │ │ ├── api.ts # Type-safe wrapper for Python API
│ │ │ └── types.ts # TypeScript interfaces for API responses
│ │ └── routes/
│ │ └── +page.svelte # Main page component
│ ├── static/ # Static assets (favicon, images)
│ └── package.json
├── scripts/
│ └── dev.py # Development server script
└── pyproject.toml # Python project configuration
The Python Api class in backend/api.py exposes methods to JavaScript:
# backend/api.py
class Api:
def greet(self, name: str) -> dict[str, Any]:
return {"message": f"Hello, {name}!"}
def get_app_info(self) -> dict[str, Any]:
return {"name": "My App", "version": "1.0.0"}
Call Python methods from Svelte using the typed wrapper in frontend/src/lib/api.ts:
// frontend/src/lib/api.ts
import { greet, getAppInfo } from '$lib/api';
// Call Python backend
const result = await greet('World');
console.log(result.message); // "Hello, World!"
const info = await getAppInfo();
console.log(info.version); // "1.0.0"
Define TypeScript interfaces that match your Python return types:
// frontend/src/lib/types.ts
export interface GreetResponse {
message: string;
}
export interface AppInfoResponse {
name: string;
version: string;
}
backend/api.py:def calculate(self, a: int, b: int) -> dict[str, Any]:
return {"result": a + b}
frontend/src/lib/types.ts:export interface CalculateResponse {
result: number;
}
frontend/src/lib/api.ts:export async function calculate(a: number, b: number): Promise<CalculateResponse> {
return getApi().calculate(a, b);
}
<script lang="ts">
import { calculate } from '$lib/api';
async function handleCalculate() {
const result = await calculate(5, 3);
console.log(result.result); // 8
}
</script>
Configure the desktop window in backend/main.py:
webview.create_window(
title="My App", # Window title
url=url, # Frontend URL
js_api=api, # Python API instance
width=1280, # Initial width
height=720, # Initial height
min_size=(800, 600), # Minimum window size
resizable=True, # Allow resizing
frameless=False, # Show window frame
easy_drag=False, # Easy window dragging
fullscreen=False, # Start in fullscreen
on_top=False, # Always on top
)
The frontend uses @sveltejs/adapter-static for static site generation, configured in frontend/svelte.config.js. This generates static HTML/JS/CSS files that pywebview serves directly.
# Python
uv run ruff check backend/ # Lint
uv run ruff format backend/ # Format
uv run mypy backend/ # Type check
# Frontend
cd frontend
npm run lint # ESLint
npm run format # Prettier
npm run check # Svelte + TypeScript check
Add pre-commit hooks for automatic code quality checks:
uv add --dev pre-commit
uv run pre-commit install
Create standalone executables that users can run without installing Python or Node.js.
First, build the frontend:
cd frontend && npm run build && cd ..
PyInstaller creates a single executable or folder containing your app and all dependencies.
# Install PyInstaller
uv add --dev pyinstaller
uv run pyinstaller \
--name "Svelte Python Desktop" \
--windowed \
--onedir \
--add-data "frontend/build:frontend/build" \
--icon "assets/icon.icns" \
--osx-bundle-identifier "com.yourcompany.svelte-python-desktop" \
backend/main.py
The .app bundle will be in dist/Svelte Python Desktop.app.
uv run pyinstaller ^
--name "Svelte Python Desktop" ^
--windowed ^
--onefile ^
--add-data "frontend/build;frontend/build" ^
--icon "assets/icon.ico" ^
backend/main.py
Note: On Windows, use
;instead of:for--add-dataseparator.
uv run pyinstaller \
--name "svelte-python-desktop" \
--windowed \
--onefile \
--add-data "frontend/build:frontend/build" \
backend/main.py
For AppImage packaging, use appimage-builder with the PyInstaller output.
For reproducible builds, create a build.spec file:
# build.spec
from PyInstaller.utils.hooks import collect_data_files
import sys
block_cipher = None
# Platform-specific settings
if sys.platform == 'darwin':
icon = 'assets/icon.icns'
separator = ':'
elif sys.platform == 'win32':
icon = 'assets/icon.ico'
separator = ';'
else:
icon = None
separator = ':'
a = Analysis(
['backend/main.py'],
pathex=[],
binaries=[],
datas=[(f'frontend/build{separator}frontend/build')],
hiddenimports=['webview'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='Svelte Python Desktop',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
icon=icon,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='Svelte Python Desktop',
)
# macOS app bundle
if sys.platform == 'darwin':
app = BUNDLE(
coll,
name='Svelte Python Desktop.app',
icon=icon,
bundle_identifier='com.yourcompany.svelte-python-desktop',
info_plist={
'CFBundleShortVersionString': '0.1.0',
'CFBundleVersion': '0.1.0',
'NSHighResolutionCapable': True,
},
)
Build using the spec file:
uv run pyinstaller build.spec
py2app creates native macOS .app bundles with better system integration.
uv add --dev py2app
Create setup.py:
# setup.py
from setuptools import setup
APP = ['backend/main.py']
DATA_FILES = [('frontend/build', ['frontend/build'])]
OPTIONS = {
'argv_emulation': False,
'iconfile': 'assets/icon.icns',
'plist': {
'CFBundleName': 'Svelte Python Desktop',
'CFBundleDisplayName': 'Svelte Python Desktop',
'CFBundleIdentifier': 'com.yourcompany.svelte-python-desktop',
'CFBundleVersion': '0.1.0',
'CFBundleShortVersionString': '0.1.0',
'NSHighResolutionCapable': True,
},
'packages': ['backend', 'webview'],
}
setup(
name='Svelte Python Desktop',
app=APP,
data_files=DATA_FILES,
options={'py2app': OPTIONS},
setup_requires=['py2app'],
)
Build the app:
# Development build (faster, for testing)
uv run python setup.py py2app -A
# Production build (standalone)
uv run python setup.py py2app
You'll need icons in different formats for each platform:
| Platform | Format | Sizes |
|---|---|---|
| macOS | .icns |
16, 32, 64, 128, 256, 512, 1024px |
| Windows | .ico |
16, 32, 48, 256px |
| Linux | .png |
256px or 512px |
# Install imagemagick
brew install imagemagick # macOS
sudo apt install imagemagick # Linux
# Create assets directory
mkdir -p assets
# Generate PNG from SVG
convert -background none frontend/static/favicon.svg -resize 1024x1024 assets/icon.png
# Generate ICO for Windows
convert assets/icon.png -define icon:auto-resize=256,128,64,48,32,16 assets/icon.ico
# Generate ICNS for macOS (requires iconutil on macOS)
mkdir -p assets/icon.iconset
for size in 16 32 64 128 256 512; do
convert assets/icon.png -resize ${size}x${size} assets/icon.iconset/icon_${size}x${size}.png
convert assets/icon.png -resize $((size*2))x$((size*2)) assets/icon.iconset/icon_${size}x${size}@2x.png
done
iconutil -c icns assets/icon.iconset -o assets/icon.icns
rm -rf assets/icon.iconset
Sign your app for distribution outside the Mac App Store:
# Sign the app
codesign --force --deep --sign "Developer ID Application: Your Name (TEAM_ID)" \
"dist/Svelte Python Desktop.app"
# Notarize with Apple
xcrun notarytool submit "dist/Svelte Python Desktop.app" \
--apple-id "[email protected]" \
--team-id "TEAM_ID" \
--password "app-specific-password" \
--wait
# Staple the notarization ticket
xcrun stapler staple "dist/Svelte Python Desktop.app"
Sign your executable with a code signing certificate:
signtool sign /f certificate.pfx /p password /tr http://timestamp.digicert.com /td sha256 /fd sha256 "dist\Svelte Python Desktop.exe"
# Install create-dmg
brew install create-dmg
# Create DMG
create-dmg \
--volname "Svelte Python Desktop" \
--window-size 600 400 \
--icon-size 100 \
--icon "Svelte Python Desktop.app" 150 200 \
--app-drop-link 450 200 \
"dist/Svelte Python Desktop.dmg" \
"dist/Svelte Python Desktop.app"
Install NSIS and create an installer script:
; installer.nsi
!include "MUI2.nsh"
Name "Svelte Python Desktop"
OutFile "dist\SveltePythonDesktop-Setup.exe"
InstallDir "$PROGRAMFILES\Svelte Python Desktop"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
Section "Install"
SetOutPath "$INSTDIR"
File /r "dist\Svelte Python Desktop\*.*"
CreateShortcut "$DESKTOP\Svelte Python Desktop.lnk" "$INSTDIR\Svelte Python Desktop.exe"
CreateDirectory "$SMPROGRAMS\Svelte Python Desktop"
CreateShortcut "$SMPROGRAMS\Svelte Python Desktop\Svelte Python Desktop.lnk" "$INSTDIR\Svelte Python Desktop.exe"
SectionEnd
Section "Uninstall"
RMDir /r "$INSTDIR"
Delete "$DESKTOP\Svelte Python Desktop.lnk"
RMDir /r "$SMPROGRAMS\Svelte Python Desktop"
SectionEnd
Build the installer:
makensis installer.nsi
Use fpm to create packages:
# Install fpm
gem install fpm
# Create DEB package
fpm -s dir -t deb \
-n svelte-python-desktop \
-v 0.1.0 \
--description "Desktop app with Svelte and Python" \
--license MIT \
dist/svelte-python-desktop=/usr/local/bin/
# Create RPM package
fpm -s dir -t rpm \
-n svelte-python-desktop \
-v 0.1.0 \
dist/svelte-python-desktop=/usr/local/bin/
Automate builds for all platforms:
# .github/workflows/build.yml
name: Build
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: |
uv sync
cd frontend && npm ci && npm run build && cd ..
- name: Build executable
run: |
uv add --dev pyinstaller
uv run pyinstaller build.spec
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}
path: dist/
This is expected when opening localhost:5173 directly in a browser. The Python backend is only available when running through pywebview. Use the development command to start the full app.
Make sure you're using Jurigged:
uv run python -m jurigged -v scripts/dev.py
On macOS, you may need to grant accessibility permissions. Also ensure you have a compatible WebKit version installed.
Clear the build cache and reinstall:
cd frontend
rm -rf node_modules .svelte-kit build
npm install
npm run build
MIT License - feel free to use this template for your own projects.