Windows Distribution Modes
scaldys-project supports three deployment modes for distributing a Python
application to Windows users. The active mode is controlled by
deployment_mode in scaldys-project.toml.
Mode |
What it builds |
When to use |
|---|---|---|
|
Binary wheel only, no installer |
Packages distributed via |
|
PyInstaller exe + Inno Setup installer |
Most applications; no Python needed on end-user machine |
|
Binary wheel + Inno Setup installer with a managed Python runtime |
Apps that must coexist with Quarto, Jupyter, or another Python environment |
Set the mode in scaldys-project.toml:
[windows]
deployment_mode = "pyinstaller" # or "pyruntime" or "wheel_only"
All modes share the Cython compilation step and produce a binary distribution wheel. What differs is how the application is launched on the end-user’s machine.
Mode 1: pyinstaller
PyInstaller bundles the project into a self-contained one-directory
executable. No Python installation is required on the end-user’s machine.
Requirements
Install the [windows] extra:
uv add --dev "scaldys-project[windows]"
This installs PyInstaller.
How it works
PyInstaller is called programmatically after the Cython step with a fixed set of options tuned for Windows distribution.
- One-directory bundle (``–onedir``)
PyInstaller runs in one-directory mode: it produces a folder containing the executable and all required libraries rather than a single merged
exe. This avoids the slow startup time of a one-file build and makes antivirus software less likely to flag the executable during extraction.- Console mode
The executable is built as a console application (
--console) so thatstdout/stderroutput is visible in a terminal or can be redirected.- UPX disabled
UPX compression is explicitly disabled (
--noupx). UPX-compressed binaries are frequently flagged by antivirus software because compression is a technique also used by malware packers.- Module collection
PyInstaller is instructed to
--collect-submodulesfor the top-level package, ensuring dynamically-imported submodules are included.- Hook file handling
If an
extra_hooks/directory exists in the compiled source tree,scaldys-projectpasses it to PyInstaller as an additional hooks directory. Hook files should be namedhook-<package_name>.py(e.g.,hook-scaldys_template.py) to be correctly discovered by PyInstaller.- Icon support
If
<project_name>.icois found in[windows] script_dir, it is passed to PyInstaller via--iconand embedded in the executable.
Output
dist/
myapp-1.2.3-cp313-cp313-win_amd64.whl
artifacts/
portable/
bin/
myapp.exe
python313.dll
_internal/ ← all imported packages and DLLs
installer/
setup.exe ← built by the subsequent Inno Setup step
The build windows command then copies launcher scripts and documentation
into artifacts/portable/ before invoking Inno Setup.
Build directories
PyInstaller’s intermediate working files are written to
build/pyinstaller/. The build clean command removes this directory
along with all other build artefacts.
Common issues
- Missing modules at runtime
PyInstaller analyses imports statically. Dynamic imports are not detected automatically. Use
--collect-submodules(already passed byscaldys-project) or add hidden imports to your hook file:# hook-myapp.py hiddenimports = ["myapp.plugins.pdf", "myapp.plugins.csv"]
- Antivirus false positives
scaldys-projectalready disables UPX to reduce the likelihood of this. If false positives persist, consider signing the executable with a code-signing certificate.- Large bundle size
PyInstaller includes every imported package. Audit your dependencies and remove unused ones. The
--exclude-moduleoption can be added to a hook file to exclude known-unnecessary packages (e.g.tkinter,unittest).
Mode 2: pyruntime
Instead of a frozen executable, the application is installed into a
uv-managed Python virtual environment (PythonRuntime) on the
end-user’s machine. The launcher scripts activate that environment.
Use this mode when your application must coexist with Quarto, Jupyter, or another tool that requires a real Python interpreter. PyInstaller is not used in this mode.
Requirements
.python-versionat the project root (single source of truth for the Python version).uv.exeavailable onPATHat build time (it is bundled into the installer for end users).Inno Setup installed on the build machine.
How it works
Cython compilation (if configured) + binary wheel build (see above).
setup_pyruntime.ps1,uv.exe, and.python-versionare staged intoartifacts/portable/bin/.The binary wheel is staged into
artifacts/portable/wheels/so the installer and setup script can find it.Launcher scripts, documentation, and examples are staged into
artifacts/portable/.Inno Setup is invoked with
/DPyruntimeMode=1.
At install time, the Inno Setup script optionally runs
setup_pyruntime.ps1 with elevated privileges to create the
PythonRuntime virtual environment inside the installation directory.
Online and offline installer sub-modes
pyruntime mode supports two sub-modes for how the Python runtime is
delivered. See Windows Installer Creation — Online and offline installer
modes for full details.
Output
dist/
myapp-1.2.3-cp313-cp313-win_amd64.whl
artifacts/
portable/
bin/
setup_pyruntime.ps1
uv.exe
.python-version
myapp_commandline.bat ← copied from script_dir
myapp_powershell.ps1
wheels/
myapp-1.2.3-cp313-cp313-win_amd64.whl
documentation/
<name>/
installer/
setup.exe
Mode 3: wheel_only
Builds only the binary distribution wheel. No installer and no launcher
scripts are produced. Use this for packages distributed via pip or
uv.
Requirements
No external tools beyond uv (already part of the build pipeline).
PyInstaller and Inno Setup are not required. The Inno Setup script
(.iss) and launcher files (.bat, .ps1) do not need to exist in
the packaging directory.
How it works
Cython compilation (if configured).
Binary wheel built and placed in
dist/.Build finishes — no installer step.
Output
dist/
myapp-1.2.3-cp313-cp313-win_amd64.whl
artifacts/
documentation/
<name>/ ← only present if public_doc_dirs is non-empty