embedded-python-launcher
Embedded Python Launcher
A pattern for creating fast-launching Python GUI applications with immediate splash screen display (<200ms) and portable deployment. Located in Arc_Segment_Calculator/launcher/.
Overview
┌────────────────────────────────────────────────────────────────┐
│ User Double-Clicks EXE │
└───────────────────────────┬────────────────────────────────────┘
│ ~50ms
┌───────────────────────────▼────────────────────────────────────┐
│ Launcher (tkinter, compiled with Nuitka) │
│ - Shows splash instantly │
│ - Spawns main app via subprocess │
└───────────────────────────┬────────────────────────────────────┘
│ ~500-2000ms (background)
┌───────────────────────────▼────────────────────────────────────┐
│ Main Application (PySide6/Qt) │
│ - Full GUI loads │
│ - Window becomes visible │
└───────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────▼────────────────────────────────────┐
│ Launcher detects main window │
│ - Closes splash │
│ - Exits │
└────────────────────────────────────────────────────────────────┘
When to Use This Skill
- Building Python GUI apps that need fast perceived startup (<200ms)
- Creating portable Python applications without system Python
- Deploying PySide6/Qt applications to users without Python
- Implementing professional splash screens
Architecture
The Problem
- PySide6/Qt takes 500-2000ms to import
- Users perceive this as slow/unresponsive
- Direct compilation includes heavy Qt libraries
The Solution
- Two-stage startup: Fast tkinter splash → Heavy PySide6 app
- Embedded Python: Self-contained Python interpreter bundled with app
- Subprocess isolation: Launcher and app are separate processes
Components
Arc_Segment_Calculator/
├── launcher/
│ ├── splash_launcher.py # Fast tkinter launcher
│ └── splash.png # Splash image
├── embedded_python/ # Self-contained Python
│ ├── python.exe
│ ├── python313.dll
│ ├── Lib/site-packages/ # PySide6, ezdxf, etc.
│ └── ...
├── src/
│ └── main.py # Main PySide6 application
├── setup_embedded_python.ps1 # Creates embedded_python/
├── build_launcher.ps1 # Compiles launcher
└── package_deployment.ps1 # Creates deploy package
Setup: Embedded Python
Running the Setup Script
cd Arc_Segment_Calculator
.\setup_embedded_python.ps1
What It Does
- Downloads Python embeddable package (python-3.13.x-embed-amd64.zip)
- Extracts to
embedded_python/ - Modifies
python313._pthto enable site-packages - Installs pip
- Installs required packages (PySide6, ezdxf)
- Cleans up cache files
Options
# Force reinstall even if exists
.\setup_embedded_python.ps1 -Force
# Specific Python version
.\setup_embedded_python.ps1 -PythonVersion "3.13.1"
# Keep pip cache (faster subsequent installs)
.\setup_embedded_python.ps1 -SkipCleanup
Result Structure
embedded_python/
├── python.exe # Executable
├── python313.dll # Runtime
├── python313.zip # Standard library (compressed)
├── python313._pth # Import path configuration
├── Lib/
│ └── site-packages/
│ ├── PySide6/ # Qt bindings
│ ├── ezdxf/ # DXF library
│ └── ...
└── Scripts/
└── pip.exe
Build: Launcher Executable
Running the Build Script
cd Arc_Segment_Calculator
.\build_launcher.ps1
Build Modes
# Standalone (default) - faster startup, multiple files
.\build_launcher.ps1 -Mode standalone
# Onefile - single executable, slower startup
.\build_launcher.ps1 -Mode onefile
# With debug console
.\build_launcher.ps1 -Debug
# Clean build
.\build_launcher.ps1 -Clean
Output
dist_launcher/
├── splash_launcher.exe # ~5-10MB executable
├── splash.png # Splash image
└── ... (support files)
Path Resolution Logic
The launcher finds resources using this search order:
Embedded Python
1. ../embedded_python/python.exe (relative to launcher)
2. ./embedded_python/python.exe (sibling to launcher)
3. ./python.exe (same directory)
4. System Python (fallback)
Main Script
1. ../src/main.py (development structure)
2. ./src/main.py (deployed with launcher)
3. ./main.py (flat deployment)
Splash Image
1. ./splash.png (same directory as launcher)
2. ../src/resources/splash.png (development structure)
SplashWindow Class
The core tkinter splash window implementation:
from launcher.splash_launcher import SplashWindow
# Create and show splash
splash = SplashWindow(
image_path=Path("splash.png"),
debug=False
)
splash.show()
# Close when done
splash.close()
Features
- Borderless window (no title bar/decorations)
- Centered on screen
- Always on top (topmost=True)
- PNG image display via tkinter PhotoImage
- Auto-closes after timeout (30s default)
- Detects main app window via Win32 EnumWindows
Main App Detection
The launcher monitors for the main application window:
# Win32 window detection (simplified)
import ctypes
def find_window_by_title(title_substring: str) -> int | None:
"""Find window handle by partial title match."""
result = []
def enum_callback(hwnd, _):
if ctypes.windll.user32.IsWindowVisible(hwnd):
length = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
if length > 0:
buffer = ctypes.create_unicode_buffer(length + 1)
ctypes.windll.user32.GetWindowTextW(hwnd, buffer, length + 1)
if title_substring.lower() in buffer.value.lower():
result.append(hwnd)
return False # Stop enumeration
return True
ctypes.windll.user32.EnumWindows(EnumWindowsProc(enum_callback), 0)
return result[0] if result else None
Command-Line Arguments
The launcher supports these arguments:
# Normal launch
splash_launcher.exe
# Debug mode (shows console, verbose output)
splash_launcher.exe --debug
# Skip splash (direct app launch)
splash_launcher.exe --no-splash
Deployment Package
Creating the Package
cd Arc_Segment_Calculator
.\package_deployment.ps1
Package Structure
deploy/
├── Arc_Segment_Calculator.exe # Launcher executable
├── embedded_python/ # Self-contained Python
│ └── ...
├── src/ # Application source
│ ├── main.py
│ └── ...
├── splash.png # Splash image
└── version.txt # Build info
Distribution
Zip the deploy/ folder and distribute to users. No Python installation required.
Performance Targets
| Metric | Target | Actual |
|---|---|---|
| Splash display | <200ms | ~50-100ms |
| Main app visible | <3s | ~1-2s |
| Launcher size | <15MB | ~8MB |
| Total package size | <200MB | ~150MB |
Creating a New Launcher
Step 1: Copy Template
# Copy launcher template
cp Arc_Segment_Calculator/launcher/ MyApp/launcher/
cp Arc_Segment_Calculator/build_launcher.ps1 MyApp/
cp Arc_Segment_Calculator/setup_embedded_python.ps1 MyApp/
Step 2: Customize Splash
Replace launcher/splash.png with your app's splash image.
Step 3: Modify Path Resolution
Edit splash_launcher.py to find your main script:
def find_main_script() -> Optional[Path]:
"""Find the main application script."""
launcher_dir = get_launcher_dir()
search_paths = [
launcher_dir.parent / "src" / "main.py", # Adjust paths
launcher_dir / "app.py",
# Add more search paths as needed
]
for path in search_paths:
if path.exists():
return path
return None
Step 4: Customize Window Detection
Modify the window title pattern to match your app:
# In splash_launcher.py
TARGET_WINDOW_TITLE = "My App Name" # Partial match
Step 5: Build and Test
# Set up embedded Python
.\setup_embedded_python.ps1
# Build launcher
.\build_launcher.ps1
# Test
.\dist_launcher\splash_launcher.exe --debug
Troubleshooting
Splash doesn't appear
- Check if
splash.pngexists in expected location - Run with
--debugto see error messages - Verify tkinter is available in Python
Main app doesn't start
- Check if embedded Python has all dependencies
- Verify
main.pypath resolution - Run
embedded_python\python.exe src\main.pymanually
Window detection fails
- Verify main app window title matches expected pattern
- Increase detection timeout if app loads slowly
- Run with
--debugto see detection attempts
Large package size
- Run
setup_embedded_python.ps1without-SkipCleanup - Remove unused packages from embedded_python
- Consider using PyInstaller for single-file if size critical
Alternatives Considered
Why Not PyInstaller Onefile?
- Slow startup (extracts to temp each time)
- No immediate splash capability
- Larger executable size for complex apps
Why Not Rust/C Launcher?
- Evaluated in
launcher/RUST_LAUNCHER_EVALUATION.md - tkinter approach sufficient for <200ms target
- Python tooling more accessible for maintenance
- Rust would add build complexity
Why Not cx_Freeze?
- Similar startup characteristics to PyInstaller
- Less active community support
- Nuitka produces faster executables
More from ds-codi/project-memory-mcp
pyside6-mvc
Use this skill when building Python desktop applications using PySide6 with strict MVC architecture where all UI is defined by .ui files. Covers architecture patterns, controller/model/view separation, signal handling, and .ui file workflows.
95pyside6-qml-views
Use this skill when creating QML view files, designing QML component hierarchies, building layouts, styling QML controls, creating reusable QML components, implementing QML navigation / page switching, or working with QML resources. Covers QML file structure, component patterns, Material/Controls styling, resource management, and common QML idioms for desktop applications.
49pyside6-qml-architecture
Use this skill when creating a new PySide6 + QML desktop application with MVC architecture, setting up project structure, implementing the application bootstrap / DI container, or understanding how the MVC layers connect. Covers project scaffolding, entry points, singleton application class, service locator, signal registry, and lifecycle management.
47mvc-architecture
Use this skill when implementing Model-View-Controller architecture. Covers core MVC principles, layer separation, dependency injection, event-driven communication, and patterns for controllers, models, and views.
40pyside6-qml-models-services
Use this skill when creating domain models with Qt signal support, implementing the repository pattern for data persistence, building service classes for external interactions, designing the central signal registry, or working with application state management. Covers BaseModel, model serialization, database repositories, service patterns, signal definitions, and the ApplicationState singleton.
34pyside6-qml-bridge
Use this skill when exposing Python objects to QML, creating bridge classes, defining Qt properties with NOTIFY signals, implementing invokable methods / slots, or connecting QML user actions to Python controllers. Covers the QObject bridge pattern, property decorators, type conversions, context properties, and QML type registration.
32