Build and deploy installable, offline-capable Python applications in the browser using PyDevices and PyScript.
This template provides a 100% standalone Progressive Web App (PWA) setup that allows your PyScript application to:
- Install natively to the user's home screen or desktop application menu.
- Work offline by caching the application shell, Python runtime, and assets.
- Run in standalone window mode without browser address bars or navigation controls.
| Host Platform | Install UX | Result |
|---|---|---|
| Desktop Chrome / Edge (Windows, macOS, Linux) | Address bar install icon or Install button | Standalone window with dedicated desktop icon |
| Android Chrome | Install app prompt or browser menu Add to Home screen | Standalone Android application window |
| Chromebook (ChromeOS) | Address bar install icon or launcher install prompt | Dedicated application window pinned to the shelf |
| iOS / iPadOS Safari | Share → Add to Home Screen | Fullscreen standalone WebKit app container |
| Smart TVs (webOS, Tizen) | Direct Chromium browser access | Runs directly in the TV web runtime |
Note
Safari on iOS does not fire the beforeinstallprompt event. Users install the app via Share → Add to Home Screen.
This template includes everything required for an offline PWA at the repository root:
pyscript-template/
├── index.html # Application shell loading PyScript, Canvas, and PWA scripts
├── main.py # Application entry point using pydevices / displaydev / appdev
├── pyscript.json # PyScript configuration: pinned PyDevices source files
├── manifest.json # Web App Manifest defining app name, icons, and theme
├── sw.js # Service Worker for caching and offline execution
├── pwa.js # Service Worker registration and install prompt handler
├── style.css # Application and UI styling
├── icon-192.png # 192x192 PNG application icon
├── icon-512.png # 512x512 PNG maskable application icon
├── vendor/pyscript/ # Vendored PyScript interpreter — generated by
│ # scripts/vendor_pyscript.sh, .gitignore'd locally,
│ # but present in every deploy (the workflow below
│ # vendors it before publishing)
├── scripts/
│ └── vendor_pyscript.sh # Downloads and unpacks the pinned PyScript release
├── tests/
│ └── test_template.py # unittest suite: config validity, asset presence, pins
├── docs/
│ ├── README.md
│ └── pwa-guide.md # this file
└── .github/workflows/
├── deploy.yml # Vendors PyScript, stamps the SW cache version, deploys to Pages
└── tests.yml # Runs the unittest suite on push/PR
manifest.json tells the browser how your application should appear when installed:
{
"name": "My PyDevices App",
"short_name": "PyDevices App",
"description": "A cross-platform Python display application built with PyDevices and PyScript.",
"start_url": "./index.html",
"scope": "./",
"display": "standalone",
"background_color": "#100e0b",
"theme_color": "#f54e00",
"icons": [
{
"src": "./icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}display: "standalone": Launches the app in its own window without browser chrome.theme_color/background_color: Sets the OS title bar and splash screen background.icons: Specifies standard and maskable icons for device home screens and app launchers.
sw.js caches all application shell resources on installation and serves cached assets when offline:
const VERSION = '__DEPLOY_VERSION__';
const CACHE_NAME = 'pyscript-template-' + (VERSION.indexOf('DEPLOY_VERSION') !== -1 ? 'dev' : VERSION);
const SHELL = [
'./',
'./index.html',
'./main.py',
'./pyscript.json',
'./manifest.json',
'./style.css',
'./pwa.js',
'./icon-192.png',
'./icon-512.png',
'./vendor/pyscript/core.css',
'./vendor/pyscript/core.js',
];
self.addEventListener('install', function (event) {
event.waitUntil(caches.open(CACHE_NAME).then(function (cache) {
return cache.addAll(SHELL);
}).then(function () {
return self.skipWaiting();
}));
});
self.addEventListener('activate', function (event) {
event.waitUntil(caches.keys().then(function (names) {
return Promise.all(names.filter(function (name) {
return name !== CACHE_NAME;
}).map(function (name) {
return caches.delete(name);
}));
}).then(function () {
return self.clients.claim();
}));
});
self.addEventListener('fetch', function (event) {
if (event.request.method !== 'GET') return;
event.respondWith(caches.match(event.request).then(function (cached) {
if (cached) return cached;
return fetch(event.request).then(function (response) {
if (!response || response.status !== 200) return response;
var copy = response.clone();
caches.open(CACHE_NAME).then(function (cache) {
cache.put(event.request, copy);
});
return response;
});
}));
});CACHE_NAME includes a version so a new cache is created whenever VERSION
changes, and the activate handler deletes every cache that doesn't match.
.github/workflows/deploy.yml replaces __DEPLOY_VERSION__ with the commit
SHA at deploy time, so every deploy automatically busts stale caches for
visitors. Locally, VERSION stays __DEPLOY_VERSION__ (rendered as dev)
across edits, so it does not change from commit to commit while you're
iterating — if you edit app files and the browser still serves the old
version, hard-refresh (or unregister the service worker in devtools) rather
than waiting for a cache change that won't happen locally.
pwa.js handles registering the Service Worker and managing the custom install prompt button:
(function () {
var installButton = document.getElementById('install');
var deferredPrompt = null;
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js', {scope: './'}).catch(console.error);
}
window.addEventListener('beforeinstallprompt', function (event) {
event.preventDefault();
deferredPrompt = event;
if (installButton) installButton.hidden = false;
});
if (installButton) {
installButton.addEventListener('click', function () {
if (!deferredPrompt) return;
deferredPrompt.prompt();
deferredPrompt.userChoice.finally(function () {
deferredPrompt = null;
installButton.hidden = true;
});
});
}
window.addEventListener('appinstalled', function () {
if (installButton) installButton.hidden = true;
});
})();-
In your GitHub repository, navigate to Settings → Pages.
-
Under Build and deployment → Source, select GitHub Actions.
-
Push to
main, or trigger it manually from the Actions tab (workflow_dispatch). The included.github/workflows/deploy.ymlworkflow:- checks out the repository,
- runs
./scripts/vendor_pyscript.shto populatevendor/pyscript/(not checked in — this step is what makes it exist in the deploy), - stamps
sw.js's__DEPLOY_VERSION__placeholder with the commit SHA, - uploads the repository root as a Pages artifact via
actions/upload-pages-artifact, and - publishes it with
actions/deploy-pages.
The application is published from the repository root — there is no separate
pwa/subdirectory to keep in sync.
Once published:
- Visit the site on desktop Chrome or Android to test the Install button.
- Visit on iOS Safari, tap the Share button, and select Add to Home Screen.
- Disconnect network connectivity to verify offline execution.