Skip to main content

portablepy

Bundle a Python app into a ZIP or tar.gz with its dependencies and a launcher. Share the archive, extract it, and run it. Dependencies install offline from the bundled wheels.

Build and inspection settings live in a project-local portablepy.toml. Specify the recipient's Python version to let the launcher reuse or download a runtime when no matching installation is available.

Install

The tool requires CPython 3.14 or later. Create a virtual environment with python -m venv .venv, then activate it with .venv\Scripts\Activate.ps1 in PowerShell or source .venv/bin/activate in a Unix shell:

python -m pip install portablepy

Build an app

Create portablepy.toml beside your script:

source = 'main.py'
run = 'python main.py'

Then run:

portablepy build

For a directory, packaged project, or wheel, change the configuration:

source = 'my-app'
run = 'python -m my_app'
output = 'dist/my-app.zip'

A wheel can use source = 'my_app-1.0-py3-none-any.whl' and run = 'my-console-command'.

Build on the operating system and architecture your users have. portablepy uses the app's .venv when available, or its own Python interpreter otherwise. The python setting can select a different build interpreter.

Without output, the archive is written beside the config file. Its name includes the app, recipient platform, architecture, and Python series. Windows gets a ZIP; Linux and macOS get a tar.gz. Set replace = true to replace an existing archive after the new build passes its checks.

Choose the recipient's Python

source = 'main.py'
run = 'python main.py'
python-version = '3.14'

python-version applies to the recipient, not the interpreter running the build. It accepts a stable CPython series such as '3.14' or an exact release such as '3.14.8'. Supported recipient versions start at 3.14. The application itself must support the selected version.

The setup helper looks for a matching installed Python on PATH, and also uses the Python launcher on Windows. It checks architecture and runtime compatibility. A series accepts any stable patch in that series; an exact release requires that patch. If no suitable installation is available, the helper looks in portablepy's shared runtime cache before downloading anything. It doesn't install Python globally or change PATH.

The build records a specific fallback release, URL, and SHA-256 hash. A series selects the latest available stable patch from the provider's index when building. A matching cached patch is reused even if the fallback is newer. Downloads must match their pinned hash.

Windows downloads come from the official Python runtime index. The general-purpose runtime ZIP retains venv, ensurepip, pip, and Tcl/Tk; documentation, headers, and development libraries are omitted from the extracted copy. The smaller embeddable distribution omits pip and Tcl/Tk and doesn't support this tool's dependency setup.

macOS and Linux use Astral's python-build-standalone releases, selected from the same download metadata used by uv. The install_only_stripped archives omit build artifacts and debug symbols. Setup also removes headers, manual pages, and the top-level static Python library. Intel/AMD x86-64 and ARM64 are supported; Linux downloads match the builder's glibc or musl environment.

Runtimes are shared by bundles belonging to the same user:

Platform Cache directory Example runtime folder
Windows %LOCALAPPDATA%\portablepy python-3.14.8-64
macOS ~/Library/Application Support/portablepy python-3.14.8-aarch64
Linux $XDG_DATA_HOME/portablepy, or ~/.local/share/portablepy python-3.14.8-x86_64-gnu

Folder names include the exact version and architecture. Linux also includes gnu or musl to keep incompatible runtimes separate. Windows architecture names follow the official index: 32, 64, or arm64. Moving an application folder keeps using the shared runtime; each application still has its own private .venv for dependencies.

The first download requires internet access. Once a suitable Python is installed or cached, launching and dependency installation work offline. no-index controls dependency package lookup, not the Python release index or runtime download.

Omitting python-version requires an installed Python matching the build environment's series and runtime characteristics.

For configured recipient versions, dependency wheels are selected for that version. Dependencies must have compatible wheels; local projects are built with the builder's interpreter, so native projects may need a prebuilt wheel for the recipient version. Python-conditional dependency markers are evaluated by pip on the build machine; declare the recipient's dependencies explicitly when those differ, and test the bundle on the recipient version. Cross-series builds require compile = 'none'. When the builder's runtime doesn't match the recipient requirements, offline installation is checked on first launch rather than during the build.

Run the bundle

Extract the whole archive somewhere writable, open a terminal in that folder, and run:

python run.py
python run.py --help

Arguments go straight to your app. With compile = 'all', use python run.pyc instead; compiled launchers require a compatible Python before they can start.

Bundles with python-version include a setup helper. If Python is missing or incompatible, run it once from the extracted folder. On Windows, use PowerShell:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./python-setup.ps1
if ($LASTEXITCODE -eq 0) {
    ./run.cmd
}

On Linux:

sh ./python-setup.sh && ./run.sh

On macOS:

sh ./python-setup.sh && ./run.command

Unix setup needs curl, tar, and either sha256sum or shasum, along with standard shell utilities. It does not require a compiler.

Setup creates the appropriate launch script after finding or downloading a suitable Python. Use that script for later launches and append application arguments normally. It handles run.py or run.pyc automatically. If an existing script wasn't created by portablepy, setup leaves it alone.

The bundle remembers its selected Python in .portablepy-python. Later launches reuse that selection without searching installed versions again. Updating the bundle or changing the interpreter invalidates the marker, so the launcher can select a suitable runtime again.

When run.py starts under an incompatible Python that can execute the launcher, it calls the setup helper automatically, creates the convenience script, and restarts with the selected runtime. A compatible installed Python launches directly without creating an extra script.

The first launch checks the bundled files and creates a private .venv using the included wheels. Later launches reuse it. Moving the bundle or updating it rebuilds the environment as needed. Python needs its standard venv and ensurepip modules.

Configuration

portablepy looks for the nearest portablepy.toml, starting in the current directory and searching its parents. You can select a different file directly:

portablepy build ./release.toml
portablepy inspect ./release.toml

All configured file paths are relative to the config file. Launch-command paths are relative to the bundled application folder. Use TOML strings for individual values, arrays for repeated values, and true or false for switches.

Setting Meaning Default
source Application directory, script, project, or wheel Config directory
run Application launch command Required
output ZIP or tar.gz archive path Generated name beside config
python Path to the build interpreter App's .venv, then portablepy's interpreter
python-version Recipient's Python series or exact release Match build runtime
requirement Array of package requirements or local package paths Import discovery or package metadata
requirements Array of requirements-file paths Adjacent requirements.txt for loose apps
extra Array of packaged application extras []
include Array of writable defaults as SOURCE=data/DESTINATION []
exclude Array of source exclusion patterns [], plus built-in exclusions
find-links Array of local wheel directories or package listing URLs []
no-index Resolve dependencies from local packages only false
compile Bytecode scope: 'none', 'app', or 'all' 'none'
strip-source Remove compiled .py files; requires compilation false
replace Replace an existing archive after build checks false
resolve Include resolved wheels in inspection reports false
profile Name of a profile defined in this file None

Optional profiles override the top-level settings. Arrays replace earlier arrays. Select the profile inside the file:

source = 'my-app'
run = 'python -m my_app'
profile = 'release'

[profiles.release]
compile = 'all'
strip-source = true
replace = true

Dependencies and files

For packaged projects and wheels, portablepy reads the package's dependency metadata. For scripts, it follows local imports and looks up dependencies in the build environment. Install your app's dependencies there before building, or declare them explicitly:

source = 'main.py'
run = 'python main.py'
requirement = ['requests>=2']
# Alternatively: requirements = ['requirements.txt']

Explicit requirements replace import-based dependency detection. Dynamic imports and plugins may need this. For packaged extras, use extra = ['cli']. For local dependency packages only, set no-index = true and find-links = ['wheels'].

The bundle includes the selected app's local helpers and resources. Common cache, environment, build, and editor files are excluded, along with .env files. Use exclude = ['*.log'] to leave out additional files.

To see what's included before building:

portablepy inspect

inspect reports files, dependencies, recipient runtime requirements, and estimated size as JSON. Set resolve = true in the config to also download or build dependency wheels for a fuller report. Inspection doesn't create a distributable or start your app. Building checks wheel integrity and, when the local interpreter matches the recipient requirements, tests installation in a clean offline environment; it doesn't start your app.

Keep writable data

source = 'my-app'
run = 'python -m my_app --config {data}/settings.json'
include = ['settings.json=data/settings.json']

Files and directories both work. Defaults live in seeds/ and are copied into data/ only when missing. Extracting an update into the same bundle folder keeps existing data. When moving to a fresh folder, copy data/ over before launching.

Launch commands support {bundle}, {app}, {data}, {python}, and {bin} placeholders. The app runs from its application folder, which is named after a content hash. Use {app} instead of hardcoding that folder name.

Commands are argument lists, so use forward slashes for paths and quote arguments containing spaces. Shell pipelines and activation commands aren't supported.

Compile to bytecode

source = 'my-app'
run = 'python -m my_app'
compile = 'all'
strip-source = true

compile = 'app' compiles application files. 'all' also compiles dependency wheels and creates run.pyc. Sources stay unless strip-source = true.

Compilation keeps assertions and docstrings at optimization level 0. Native extensions, resources, and dependency licenses stay in the bundle. Some packages need their source files at runtime, so test your app before distributing a source-stripped build. Bytecode is specific to the Python series and doesn't hide your code securely.

Check a bundle

portablepy verify app.zip
portablepy verify ./extracted-app
python run.py --portable-info
python run.py --portable-setup
python run.py --portable-verify

Use run.pyc for bundles built with compile = 'all'. After runtime setup, you can also pass these arguments to the generated script, such as ./run.cmd --portable-verify on Windows or ./run.sh --portable-verify on Linux.

--portable-info shows bundle details without setting up the application environment. --portable-setup prepares the environment without starting the app. --portable-verify checks bundled files and leaves writable data alone.

Each archive includes checksums and gets a .sha256 sidecar. These detect damaged or changed files; they don't prove who published the bundle.

Migrating existing builds

Build and inspect no longer accept option flags or source paths. Move settings from [tool.portablepy] in pyproject.toml into a dedicated portablepy.toml, without that table header. Rename [tool.portablepy.profiles.release] to [profiles.release] and choose it with profile = 'release' at the top of the file.

Former flags use the same names as config keys: --run becomes run, --strip-source becomes strip-source = true, and repeated --requirement values become a requirement array. Use strip-source = false, no-index = false, and replace = false instead of the old inverse flags. --command maps to run, -o to output, and --compile-mode to compile. Supply a config path as the positional argument instead of --config.

Development

Activate the project's .venv, then run:

python -m pip install -e ".[dev]"
python -m pytest --cov=portablepy --cov-branch
python -m ruff check .
python -m ruff format --check .
python -m mypy
python -m argly gen --package portablepy.commands --name portablepy --output src/portablepy/generated.py --check

Regenerate src/portablepy/generated.py with the same Argly command without --check after changing command definitions or help text.

Metadata

Release files for portablepy 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for portablepy 0.3.1
File Size Uploaded Provenance Actions
portablepy-0.3.1.tar.gz 67.4 kB With provenance Details for portablepy-0.3.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for portablepy 0.3.1
File Interpreter ABI Platform Actions
portablepy-0.3.1-py3-none-any.whl Python 3 none any Details for portablepy-0.3.1-py3-none-any.whl

Total release size: 119.4 kB

Release files / portablepy-0.3.1.tar.gz

Download URL portablepy-0.3.1.tar.gz
Size 67.4 kB
Tags Source
SHA-256 checksum
How to use checksums
624e30580bb64046b67299c7e71f10dc6b646fae1450c38ab2f8b257c041e6b0
BLAKE2b-256 checksum
How to use checksums
77eecd6278f6c00825f9df05af55a6bcfc4e0815c0e02150ee769ceb1c642984
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / portablepy-0.3.1-py3-none-any.whl

Download URL portablepy-0.3.1-py3-none-any.whl
Size 52.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f601121754175a2fa8fa620db90b12d3e3836ccd78e4ab1f0ab6d8e9c6a7951a
BLAKE2b-256 checksum
How to use checksums
15e0bb01178a90d23c73c701385ee3d9de5b5ddbb0ef6f20ae11744db0987a38
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Security for portablepy 0.3.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for portablepy-0.3.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page