Skip to content

Creating Standalone Python CLI Executables in 2024-2025

PyInstaller remains the most practical choice for Python CLI distribution in late 2024, offering the best balance of ease-of-use, build speed, and compatibility—but modern alternatives like Nuitka deliver superior performance for compute-intensive tools. For projects using uv, integration requires some workarounds, though PyInstaller 6.16.0 (September 2025) introduced critical compatibility fixes. Cross-platform distribution demands platform-specific builds but can be fully automated through GitHub Actions. Notably, the Windows code signing landscape changed dramatically in March 2024 when Microsoft eliminated instant SmartScreen reputation for EV certificates, fundamentally altering the cost-benefit calculus for developers.

The Python executable packaging ecosystem has matured significantly, with unified CI/CD approaches now replacing platform-specific build systems for most projects. Modern CLI frameworks like Rich and Typer work reliably in packaged executables once proper metadata collection is configured. Alternative approaches—particularly pipx installation from PyPI—have emerged as the community-preferred method for developer-facing tools, while truly standalone binaries remain essential for end-user applications. For a CLI compiler tool processing text to MIDI, PyInstaller's onedir mode offers the fastest builds (3-4 minutes versus Nuitka's 10-30 minutes) with minimal runtime overhead, making it ideal for iterative development.

PyInstaller dominates for general-purpose CLI tools

PyInstaller has cemented its position as the most recommended Python-to-executable tool in 2024-2025, with version 6.16.0 released in September 2025 supporting Python 3.8 through 3.13. The tool bundles your Python application with the interpreter and all dependencies into either a single executable (onefile mode) or a directory of files (onedir mode). For CLI tools specifically, onedir mode proves superior—producing 46-113 MB distributions across platforms with startup times of 0.32-0.79 seconds, essentially matching native Python performance.

The build process itself completes in just 3-4 minutes, dramatically faster than alternatives. PyInstaller's extensive hook system handles complex dependencies automatically, with the community-maintained pyinstaller-hooks-contrib package receiving monthly updates through 2025. The tool's 12,700-star GitHub repository shows consistent maintenance with monthly releases, and its documentation ecosystem dwarfs competitors with 10x more Stack Overflow questions and answers than alternatives.

Critical to modern workflows, PyInstaller integrates well with pyproject.toml-based projects. You simply install your project normally, then run PyInstaller as a post-build step. The tool automatically analyzes imports and bundles dependencies, though dynamic imports occasionally require manual specification via hiddenimports in the spec file. For a MIDI compiler tool, this means straightforward packaging of libraries like mido or music21 without extensive configuration.

The primary trade-off involves file size—PyInstaller executables run larger than Nuitka's compiled output because they bundle the full Python interpreter. Code also remains easily decompilable since PyInstaller freezes bytecode rather than compiling to C. However, for most CLI tools where distribution size and intellectual property protection aren't primary concerns, these limitations prove acceptable given PyInstaller's reliability and community support.

Nuitka delivers performance through true compilation

Nuitka takes a fundamentally different approach by transpiling Python to C, then compiling to native machine code. Version 2.5.x released in November 2024 supports Python 3.13 including experimental free-threaded mode, positioning it at the cutting edge of Python development. This true compilation delivers 20-300% performance improvements for compute-intensive code—the tool consistently achieves the fastest execution times in benchmarks at 0.25-0.62 seconds startup versus PyInstaller's 0.32-0.79 seconds.

The performance gains become more pronounced for CPU-bound operations like parsing complex text formats or algorithmic processing, making Nuitka particularly attractive for a compiler tool. The compiled executables also provide better code obfuscation since the Python code no longer exists in any recoverable form. Nuitka can even create extension modules rather than just executables, offering flexibility for projects that need both library and CLI interfaces.

However, these advantages come with significant costs. Build times extend to 10-30 minutes compared to PyInstaller's 3-4 minutes, creating friction during iterative development. The tool requires a C++ compiler (gcc, clang, or MSVC) installed on the build system, adding complexity to CI/CD pipelines. Onedir mode executables also run larger than PyInstaller's at 92-166 MB, though onefile mode achieves slightly smaller sizes at 22-41 MB.

Nuitka's single-maintainer development model, while commercially backed, presents some risk compared to PyInstaller's larger contributor base. Configuration for complex packages can prove more intricate, requiring specialized plugins. For projects where build time outweighs runtime performance—typical during active development—PyInstaller's rapid iteration cycle proves more practical. But for production distributions of performance-critical tools, Nuitka's compilation approach delivers measurable benefits.

PyOxidizer sits effectively abandoned while cx_Freeze remains viable

PyOxidizer's promising Rust-based approach has fallen victim to maintainer burnout. Creator Gregory Szorc announced in March 2024 that he was stepping back from the project, describing himself as "disenchanted with Python ecosystem." The tool hasn't seen releases since November 2022 and remains stuck supporting only Python 3.8-3.10—making it unsuitable for any new project targeting modern Python versions. The Algorand team explicitly rejected PyOxidizer for their CLI tool in their documented January 2024 evaluation, and Snyk now lists the maintenance status as "Inactive."

The tool's original vision—embedding Python in Rust binaries with zero-copy memory imports for optimal startup performance—represented genuinely innovative design. However, the complexity of maintaining Rust-Python integration alongside Python's rapid evolution proved unsustainable for a single maintainer. Projects currently using PyOxidizer face migration decisions, with PyInstaller and Nuitka representing the most straightforward alternatives.

cx_Freeze maintains active development with version 8.4.1 released in September 2025, supporting Python 3.9-3.13 with experimental 3.14 support. The tool follows a similar bytecode-freezing approach to PyInstaller but with a smaller hook ecosystem and community. One notable limitation: cx_Freeze cannot create single-file executables, producing only onedir distributions. This makes it less versatile than PyInstaller for scenarios where single-file deployment simplifies distribution.

For Windows-only projects, py2exe (version 0.14.0.0 in 2024) and macOS-only projects, py2app (version 0.28.8 in 2024) continue receiving updates. However, for cross-platform CLI tools, PyInstaller's unified approach proves more maintainable than juggling platform-specific tools. These specialized packagers serve niches but lack the comprehensive testing and community resources that multi-platform tools benefit from.

Integration with uv requires specific workarounds

Astral's uv package manager has revolutionized Python dependency management with 10-100x speed improvements over pip, but it deliberately focuses on package management rather than executable building. The tool provides no native support for creating standalone binaries—feature requests like issue #5802 garnered 200+ reactions but remain marked "wish" and off the immediate roadmap. Using uv build generates wheels and source distributions, not executables for end-user distribution.

PyInstaller integration with uv-managed projects has improved substantially through 2024. PyInstaller 6.16.0 specifically addressed "discovery and collection of Python shared library when using uv-installed or rye-installed Python" on Linux. Earlier issues included Windows binary generation problems and tkinter/TCL detection failures, though workarounds now exist for remaining edge cases. The recommended workflow involves adding PyInstaller as a dev dependency via uv add --dev pyinstaller, then running builds through uv run pyinstaller to ensure the correct environment.

Nuitka presents more challenges with uv. Release 2.7 in late 2024 added explicit support for Python Build Standalone distributions used by uv, acknowledging the integration need. However, active issues as of January 2025 report segmentation faults when using uv-installed Python, and Linux builds fail with libpython not found errors. The Nuitka team recommends using system Python rather than uv's Python distribution for production builds until compatibility matures. This creates an awkward workflow where developers use uv for dependency management but must switch to system Python for final executable creation.

The practical hybrid approach that works most reliably separates concerns: use uv for development dependency management, then export to requirements.txt with uv export --format requirements-txt for traditional builds. This allows leveraging uv's speed during development while using battle-tested system Python for distribution. Your CI/CD pipeline installs Python via actions/setup-python, installs dependencies from the exported requirements, then runs PyInstaller or Nuitka against the stable environment. While less elegant than end-to-end uv integration, this pattern proves reliable across platforms and avoids the sharp edges of cutting-edge tooling.

Unified CI/CD eliminates the need for separate build systems

Modern cross-platform binary distribution has converged on GitHub Actions with matrix builds as the standard approach. Rather than maintaining separate build infrastructure for Windows, Linux, and macOS, projects define a single workflow that spawns parallel builds across platforms. This represents a fundamental shift from earlier practices where platform-specific build systems created maintenance burdens and inconsistencies.

A basic matrix build covers the essentials in under 50 lines of YAML. The strategy matrix lists operating systems—typically ubuntu-latest, windows-latest, and macos-latest—with GitHub providing native runners for each platform. Actions like actions/setup-python and actions/checkout standardize the environment setup, then your build commands run identically across platforms. PyInstaller specifically requires building on each target platform since it's not a cross-compiler, making this parallel approach essential rather than optional.

For Linux CLI tools, AppImage has emerged as the recommended distribution format. Unlike Snap or Flatpak, AppImage requires no installation infrastructure—users download a single portable file and run it directly. AppImages achieve the fastest startup times among containerized formats and work across Ubuntu, Debian, Fedora, Arch, and other major distributions without systemd dependencies. The format proves ideal for command-line tools where installation friction reduces adoption.

Windows distribution typically combines a standalone .exe with optional installer packages. PyInstaller's --onefile flag produces a single executable that extracts dependencies to a temporary directory at runtime. For more polished distribution, wrapping the executable with Inno Setup creates a proper installer with desktop shortcuts, start menu integration, and uninstall support. Inno Setup has provided free, reliable Windows installers since 1997 and remains the community favorite for Python applications. The wizard interface generates installer scripts quickly, avoiding the complexity of WiX Toolset or the cost of Advanced Installer.

macOS presents the most complex requirements due to Apple's security infrastructure. Creating an .app bundle packages your executable with required metadata and resources, but distribution requires both code signing and notarization. The Developer ID certificate costs $99/year through the Apple Developer Program, a mandatory expense for any serious macOS distribution. The notarization process submits your signed app to Apple's servers for malware scanning, usually completing in minutes to hours. Once approved, you staple the notarization ticket to your .app bundle, enabling it to pass Gatekeeper checks on user systems.

GitHub Actions can fully automate macOS signing and notarization by storing certificates as base64-encoded secrets, importing them into the runner's keychain during the build, performing the signing and notarization, then cleaning up credentials. This automation proves crucial since manual signing on developer machines doesn't scale and introduces security risks from credential exposure. The Apple-Actions/import-codesign-certs action handles much of the complexity, though deprecated alternatives exist.

The cibuildwheel tool has become standard for Python packages with C extensions, used by NumPy, SciPy, pandas, and PyTorch. It automatically builds manylinux, musllinux, macOS, and Windows wheels while handling platform-specific compilation requirements and dependency bundling. However, cibuildwheel targets wheel distribution for pip installation rather than standalone executables. For pure-Python CLI tools creating executables, the simpler PyInstaller-based matrix build suffices.

CLI frameworks require specific packaging considerations

Modern Python CLI development heavily relies on frameworks like Click, Typer, and Rich for argument parsing, type-safe interfaces, and beautiful terminal output. These libraries generally work well in packaged executables but require specific attention during the packaging process. Understanding these requirements prevents frustrating deployment-time failures after successful local testing.

Click applications face entry point issues because PyInstaller executables lack the normal entry point machinery. The solution involves detecting the frozen state with getattr(sys, 'frozen', False) and calling the CLI function directly with sys.argv when frozen, rather than relying on Click's automatic invocation. This pattern has become standard practice documented across Stack Overflow and PyInstaller recipes.

Typer adds complexity through its shellingham dependency for shell completion features. PyInstaller doesn't automatically detect shellingham.posix and shellingham.nt as required imports, causing ModuleNotFoundError at runtime. The spec file must explicitly list these as hiddenimports. Many developers skip shell completion in frozen executables entirely since users would need to install completion separately anyway, simplifying the packaging.

Rich's beautiful terminal formatting works correctly in frozen executables once metadata collection is configured. PyInstaller 5.0+ improved metadata handling significantly, but older versions or certain configurations may encounter "The 'rich' distribution was not found" errors. The fix involves using copy_metadata('rich') in your spec file's datas section, ensuring Rich can discover its version and features at runtime. Progress bars, syntax highlighting, and terminal detection all function properly once metadata issues are resolved.

Terminal color support across Windows, macOS, and Linux requires defensive coding. While macOS and Linux have supported ANSI escape codes natively for years, Windows only gained support in Windows 10 version 1511 (2015). The colorama library provides a compatibility layer that auto-converts ANSI codes on older Windows versions, and it works seamlessly in frozen executables without special configuration. Following NO_COLOR and CLICOLOR environment variable standards ensures your tool respects user preferences and integrates well with piped output.

Windows console attachment presents the most frustrating platform-specific issue. Using PyInstaller's --noconsole flag (intended for GUI apps) on a CLI tool causes disaster—sys.stdin, sys.stdout, and sys.stderr all become None, breaking any print statements, input calls, or subprocess operations. CLI tools must always use console mode. Additionally, subprocess calls in console mode can cause brief flashing windows unless you explicitly set STARTUPINFO flags on Windows to hide child process consoles.

Testing packaged CLI tools requires verifying piping and redirection work correctly. Users expect mytool | grep to filter output and mytool > file.txt to redirect results. This requires checking sys.stdout.isatty() to detect piped output and adjusting behavior accordingly—disabling progress bars and fancy formatting when output is being redirected. PyInstaller preserves these system-level behaviors, but untested assumptions in your code may cause failures in production that never appeared during development.

Alternative distribution through pipx has become community standard

The Python packaging landscape increasingly favors pipx for developer-facing CLI tools over standalone executables. pipx creates isolated virtual environments for each installed tool while exposing them globally on PATH, solving the perennial problem of dependency conflicts between CLI utilities. Think of it as "npm's npx for Python"—the tool even earned official PyPA endorsement and active maintenance.

Installing a tool via pipx takes one command: pipx install httpie. This creates a dedicated virtual environment in ~/.local/share/pipx/venvs/ containing just that tool and its dependencies, then symlinks the executable to ~/.local/bin/. Users gain global access to the http command without any risk that httpie's dependencies conflict with other tools' requirements. Upgrading proves equally simple with pipx upgrade httpie or pipx upgrade-all for bulk updates.

Performance matches native Python execution since pipx avoids any containerization or extraction overhead—startup times run 50-150ms, identical to regular pip installations. Storage footprint runs higher than system-wide pip since each tool maintains separate dependency copies, typically consuming 20-200 MB per tool depending on complexity. However, this remains far smaller than Docker images while providing complete isolation.

The community has largely standardized on recommending pipx installation for Python CLI tools. Modern project README files increasingly show pipx install mytool as the primary installation method, with pip install mytool provided as a fallback for users without pipx. Real Python, the Python Packaging Authority documentation, and development environment setup guides all endorse this pattern. Tools like poetry, black, httpie, awscli, tox, cookiecutter, and pre-commit document pipx as the preferred installation method.

Python's zipapp module and the third-party shiv tool offer true single-file distribution. zipapp creates executable ZIP archives (.pyz files) that run with python myprog.pyz, while shiv extends this by automatically bundling all dependencies. On first run, shiv extracts compiled extensions to ~/.shiv/ for subsequent fast execution. This approach works well for internal tools in air-gapped environments or situations where distributing a single file simplifies deployment. However, the requirement for Python to be pre-installed on target systems limits applicability for end-user tools.

Docker-based distribution solves complex dependency scenarios but introduces significant overhead for CLI tools. Container initialization adds 500ms-2s to every invocation, making Docker suitable for long-running processes or complex multi-service applications but painful for quick command-line utilities. Users must install Docker itself—a substantial requirement—and then work with verbose docker run commands or create wrapper scripts. For simple CLI tools, Docker proves overkill, though it excels when system-level dependencies or reproducible environments matter more than startup latency.

Standard wheel distribution via PyPI with console_scripts entry points remains fundamental for libraries that also provide CLI interfaces. The workflow proves familiar to Python developers: pip install creates the package in site-packages and generates executable wrappers automatically. However, this approach shares the Python interpreter across all installed packages, creating the dependency conflicts that pipx explicitly solves. Modern best practice suggests publishing wheels to PyPI but recommending pipx installation to users.

Real-world projects increasingly adopt Rust for performance

Examining how successful modern Python CLI tools actually distribute reveals clear patterns. The most notable trend: high-performance tools increasingly use Rust implementations rather than Python, even when targeting Python developers. This shift fundamentally changes the distribution model from Python packaging to native binary releases.

Ruff exemplifies this approach perfectly. Written entirely in Rust, the linter and formatter compiles to 12-13 MB standalone binaries for Linux, macOS, and Windows across x86_64, ARM64, and various other architectures. Users can install via standalone installer scripts (install.sh/install.ps1), download pre-built binaries from GitHub Releases, or install via package managers like Homebrew and Conda. Astral also publishes a Python wrapper package to PyPI that shells out to the embedded Rust binary, maintaining pip install compatibility. This architecture delivers 10-100x performance improvements over pure Python tools like Black and Flake8 while simplifying distribution.

The uv package manager follows an identical pattern—pure Rust binary distributed as standalone installer or via GitHub Releases, with no Python dependency required to run. uv even manages Python installations itself, creating a self-contained development environment bootstrapper. With 16M+ downloads monthly, uv demonstrates that the Python community readily adopts Rust-based tooling when it provides substantial speed improvements.

The maturin build tool facilitates this Rust-Python integration pattern. It uses PyO3 for Rust-Python bindings and produces both standalone binaries and Python wheels containing compiled extensions. The workflow involves writing Rust code with PyO3 annotations, running maturin build to create platform-specific wheels, then distributing via PyPI or as binaries. This approach has become standard practice for the scientific Python ecosystem, with tools like cryptography, numpy, and polars using PyO3/maturin for performance-critical components.

For pure Python tools avoiding Rust, yt-dlp demonstrates comprehensive PyInstaller usage. The popular video downloader publishes standalone executables for Windows (x86_64, x86), macOS (x86_64, ARM64), and Linux (x86_64, ARM64, ARMv7l) via GitHub Releases. They also provide zipapp format as a fallback and maintain nightly/master builds for cutting-edge features. The project includes THIRD_PARTY_LICENSES.txt documenting all bundled dependencies—a best practice for legal compliance. Built-in update functionality (yt-dlp -U) allows the tool to self-update, maintaining fresh installations without user intervention.

Traditional developer tools like black, mypy, and poetry mostly avoid standalone binaries, relying instead on PyPI distribution with pip or pipx installation. mypy offers an interesting middle ground with mypyc wheels—the type checker uses its own compilation infrastructure to compile type-annotated Python to C extensions, achieving 4x speedups while remaining installable via pip. This hybrid approach delivers performance without requiring full Rust rewrites.

For a compiler or transpiler tool, examining py2many, transpyle, and Transcrypt provides relevant patterns. These projects remain pure Python distributed via PyPI, accepting that their target audience (developers) has Python installed. The compilation complexity lives in the Python codebase, with the tools generating C, Rust, C++, JavaScript, or other target languages as output. They focus on correctness and maintainability over raw performance since compilation typically occurs during build processes rather than tight inner loops.

Code signing presents significant but navigable challenges

Windows code signing underwent a seismic policy shift in March 2024 that fundamentally altered cost-benefit calculations. Previously, expensive Extended Validation (EV) certificates provided immediate SmartScreen reputation, allowing newly-signed applications to avoid security warnings. Microsoft eliminated this instant reputation benefit, making EV and standard Organization Validation (OV) certificates functionally equivalent for SmartScreen purposes. Both certificate types now require building reputation over weeks or months through download counts and user feedback, with no documented threshold for when warnings disappear.

This change makes standard OV certificates (\(129-\)340/year) the rational choice over EV certificates (\(296-\)507/year) since the premium buys no meaningful advantage. As of June 2023, both require storage on FIPS 140-2 Level 2 USB tokens or cloud HSMs, complicating CI/CD integration but remaining manageable with proper secrets handling. The timestamping feature—essential for keeping signatures valid after certificate expiration—works identically for both certificate types.

Unsigned Windows executables face "Windows protected your PC" SmartScreen warnings requiring users to click "More info" then "Run anyway." Beyond the security warnings, PyInstaller and other Python packagers frequently trigger antivirus false positives. VirusTotal scans typically show 2-10 vendors flagging new unsigned builds, including major players like Windows Defender, Norton, Kaspersky, and Bitdefender. Developers spend significant time submitting false positive reports to each vendor, with Microsoft responding in hours to days but smaller vendors taking weeks or proving difficult to contact. Each new release may trigger fresh flags, and whitelisting only applies to the specific executable hash.

Signing doesn't eliminate false positives entirely but significantly reduces their frequency. However, the reputation-building period remains frustrating—new developers report several weeks of SmartScreen warnings before sufficient download volume establishes trust. Each new application from the same certificate may inherit some reputation, providing marginal benefit for developers with multiple projects.

macOS distribution without proper code signing and notarization creates even more severe barriers. Gatekeeper blocks unidentified developers with prominent warnings, requiring users to right-click and select Open—an unintuitive workaround that many users never discover. MacOS 10.15 Catalina and later versions increasingly restrict unsigned software, making professional distribution essentially impossible without an Apple Developer Program membership ($99/year).

The notarization process requires a Developer ID Application certificate, hardened runtime enabled during signing, and submission to Apple's notary service for malware scanning. The xcrun notarytool command (replacing deprecated altool as of November 2023) handles submission, with Apple typically completing scans in minutes to hours. Once approved, you staple the notarization ticket to your app bundle using xcrun stapler, enabling it to pass Gatekeeper checks on user systems. PyInstaller executables require special attention since their structure differs from typical macOS apps—using --osx-bundle-identifier properly and ensuring all embedded binaries are signed prevents notarization failures.

GitHub Actions automation can handle the entire signing and notarization workflow. Certificates convert to base64 for storage in GitHub Secrets, get decoded during the build, imported to the runner's keychain, used for signing, then deleted. The secrets never appear in logs and workflows can restrict access to specific branches or contributors. Multiple pre-built actions exist on the GitHub Marketplace, though some face deprecation as Apple changes tooling requirements.

For open-source projects with limited budgets, SignPath Foundation and Sigstore provide free alternatives. SignPath offers free code signing for qualifying open-source projects through certificates issued to the project itself (not individuals), with HSM-based key storage and integration with automated builds. The service verifies that binaries built from the OSS repository match what's being signed, adding supply chain security.

Sigstore represents a more fundamental shift toward keyless signing using OIDC identity. Backed by the OpenSSF, Sigstore recorded 46M+ signatures across 22,000+ GitHub projects as of 2023. npm achieved general availability of Sigstore signing in 2024, Homebrew adopted it for packages, and PyPI integration is underway. For new projects especially, Sigstore offers the most future-proof free signing solution, though adoption remains early enough that platform support varies.

Many small open-source projects simply skip signing, documenting workarounds for users and accepting security warnings as the cost of free software. This proves acceptable for tools targeting technical audiences who understand the tradeoffs and can verify checksums manually. For developer tools distributed via package managers like pipx, Homebrew, or apt, the package manager itself provides trust boundaries, reducing the need for application-level signing.

Practical recommendations for a CLI compiler tool

Building a compiler for Music Macro Language to MIDI as a Python CLI tool benefits from specific tooling choices based on these findings. Start development using uv for dependency management—its speed dramatically improves iteration cycles compared to pip. Structure your project with pyproject.toml following modern Python packaging standards, defining your CLI entry point in [project.scripts]. This foundation works whether you ultimately distribute as a standalone executable or via PyPI.

For standalone executable distribution, choose PyInstaller over alternatives. The 3-4 minute build times during development prove invaluable compared to Nuitka's 10-30 minutes, and MIDI file processing likely doesn't benefit from Nuitka's compilation speed improvements since your tool will be I/O bound rather than CPU bound. Use onedir mode rather than onefile—the slightly larger distribution (one folder versus one file) matters less than the 3x faster startup time that avoids extraction delays.

If you use Rich for progress bars during parsing or Typer for CLI interface, explicitly configure metadata collection in your PyInstaller spec file. Add copy_metadata('rich') to datas and include shellingham modules in hiddenimports. Test your packaged executable with actual MIDI files to ensure file I/O works correctly and any subprocess calls (if invoking external MIDI tools) handle Windows console attachment properly.

Set up GitHub Actions for cross-platform builds from the start rather than building locally. A matrix build covering ubuntu-latest, windows-latest, and macos-latest enables parallel compilation, with builds completing simultaneously rather than sequentially. For Linux distribution, create an AppImage to provide a single portable file that works across distributions without installation. Windows users get a standalone .exe, optionally wrapped with an Inno Setup installer if you want Start Menu integration.

Skip code signing initially unless you're targeting enterprise users. For an open-source MIDI compiler tool, document the installation workarounds for Windows SmartScreen and macOS Gatekeeper in your README. Once the project gains traction, apply for SignPath Foundation certification or adopt Sigstore for free signing. If pursuing commercial distribution, budget $100 for Apple Developer Program membership (essential for macOS) and $200-300 for a Windows OV certificate (don't overpay for EV).

Consider dual distribution: publish to PyPI with pipx installation as the primary method for developers, and provide standalone executables via GitHub Releases for musicians who may not have Python installed. The PyPI package can be simpler—just wheels without bundled executables—while GitHub Releases provide the full standalone experience. This two-track approach serves both technical users who prefer pipx's isolation and non-technical users who want "download and run."

Structure your compiler with a clear separation between parsing, compilation, and MIDI generation phases. This modularity helps if you later decide to extract performance-critical components to Rust using PyO3/maturin. Many developers start with pure Python, profile to identify bottlenecks, then selectively rewrite hot paths in Rust while keeping the overall architecture in Python. This incremental approach avoids premature optimization while maintaining a path to peak performance if needed.

Test your executables on clean systems without Python installed—virtual machines or Docker containers prove valuable for validation. Verify that MIDI output works correctly, file paths handle spaces and special characters, and error messages remain helpful rather than showing cryptographic PyInstaller internals. The difference between a tool that works locally and one that distributes successfully lies in these unglamorous testing details that catch environment-specific assumptions.