Automate Python Binary Distribution with Py2Native
You already automate tests, linting, and release tags. But Python binary distribution often remains a manual chore: set up a pristine environment, freeze dependencies, compile for each platform, and then ship a bundle that still contains readable .py files unless you bolt on extra obfuscation. This article is about eliminating that repetitive step. Py2Native turns your plain Python into native machine code with one command, so the artifact you distribute is a compiled binary instead of source.
If you have been looking at freeze-and-bundle tools or hand-written Cython builds, Py2Native takes a different path: your custom Python becomes native code, while third-party libraries stay as normal Python packages and work as-is. No .pyx files, no manual cythonize step, no C extensions to write.
The Manual Grind of Python Binary Distribution
Most Python teams that ship proprietary software know the routine too well. It looks something like this:
- Recreate a clean virtual environment on every build machine.
- Pin the exact CPython version and freeze dependencies so the artifact is reproducible.
- Add packaging steps for Windows, Linux, and macOS because a
.pyd,.so, or.dylibhas to be built per platform. - Verify that the target machine has a compatible Python interpreter and the right runtime libraries.
- Decide whether to ship plain source, obfuscated source, or a hand-maintained Cython/C extension toolchain.
That last decision is where the real pain starts. Shipping .py files exposes your intellectual property. Obfuscators add complexity and can still be reversed. Hand-written Cython requires learning Cython syntax, maintaining .pyx and .pxd files, and orchestrating builds manually.
Py2Native automates the repetitive part. The compiler uses Cython under the hood, but hides it completely. You write plain Python, pass your entry point and source files to uv run py2native build, and get a native executable or importable shared library. The pipeline handles Cython-to-C conversion, C compilation, linking, and optional wheel or embed packaging.
One-Time Setup: From Zero to Compiling
Before the first automated build, you need three things on the machine:
- CPython 3.11 through 3.15, including free-threaded 3.14t and 3.15t
- A platform C compiler and linker: MSVC on Windows, GCC on Linux, or Clang on macOS
- Internet access so Py2Native can fetch the Python distributions and libraries it needs
Py2Native is driven through uv, never pip. Inside your project, add it as a development dependency and verify the CLI:
mkdir myapp && cd myapp
uv init
uv add --dev py2native
uv run py2native --help
uv run py2native build main.py *.py
The command above compiles main.py and every other matched .py file into a native executable. There is no separate cythonize invocation, no setup file for C extensions, and no platform scaffold to maintain.
For paid distributions that need license enforcement, install the closed-source py2nativepro plugin and generate a signing keypair:
uv run py2native keygen private.pem public.pem
Keep private.pem out of the repository. The public key is the only key that gets baked into a build; the private key stays in your signing environment, such as CI secrets.
The Automated Pipeline: Build, Package, Deploy
The core workflow is a single build step:
uv run py2native build main.py *.py
That compiles all source globs into a native executable. Py2Native expands the globs under the current base directory, dispatches through its plugin manager, converts Python to C with Cython, compiles the generated C files, and links the final artifact.
Library mode for reusable packages
When you ship an importable package instead of an app, use --library:
uv run py2native build src/mypkg/core.py src/mypkg/utils.py --library --wheel dist/
Library mode creates a compiled shared library — .pyd on Windows, .so on Linux, .dylib on macOS — that contains all your modules. The wheel builder then adds two small files:
__init__.py, which bridges Python’s import system to the native library__main__.py, which enablespython -m mypkg
Both files are required and generated for you. You do not hand-write import hooks or bootstrap logic.
Wheel mode for standard distribution
Use --wheel to produce a PEP 427 wheel:
uv run py2native build main.py *.py --wheel dist/
The resulting wheel contains the compiled binary and can be published to PyPI or an internal package index. Consumers install it like any normal package. Your code inside is native, not readable source.
Embed mode for self-contained deployment
For a deployment directory that includes the compiled binary and all dependencies, use --embed:
uv run py2native build main.py *.py --embed deploy/myapp
Py2Native creates a uv-managed directory, so you can ship a complete runtime without asking customers to set up Python environments manually.
Putting it in CI/CD
Because the entire tool is a CLI, it fits naturally in GitHub Actions, GitLab CI, or any other runner. A basic automated build on every tag looks like this:
name: build-native
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv add --dev py2native
- run: uv run py2native build src/myapp/main.py src/myapp/*.py --wheel dist/
- uses: actions/upload-artifact@v4
with:
name: native-wheel
path: dist/
Build on multiple operating systems with a matrix strategy when you need per-platform artifacts. The command stays the same; Py2Native’s platform plugins apply the correct compiler and linker flags on Windows, Linux, and macOS.
Signing licenses and embedding verification
If you use the Pro plugin, the build step can require a valid signed license. First, create a JWT payload and sign it:
{
"iss": "RSJ Software GmbH",
"aud": "TimestampGIT",
"sub": "customer-123"
}
uv run py2native sign --private private.pem payload.json license.dat
Then build with license verification enabled:
uv run py2native build main.py *.py --license license.dat --public public.pem
Inside your application, call the verifier that Py2Native bakes into the generated bootstrap. The Pro plugin injects _runtime_verify_es256_jwt into your compiled namespace, so your plain Python code can gate startup on a valid license.
To expose that function during compilation, add a small .pxd declaration file that Py2Native consumes automatically:
# py2native_license.pxd
from _p2n_bootstrap cimport _runtime_verify_es256_jwt
cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)
Then in your application:
from pathlib import Path
def read_license_file(path):
return Path(path).read_text().strip()
def main():
licenseString = read_license_file("license.dat")
license = _runtime_verify_es256_jwt(
licenseString,
expected_iss="RSJ Software GmbH",
expected_aud="TimestampGIT",
)
# Execution reaches here only if the JWT signature and claims are valid.
start_application()
Only the public key is stored in the executable. The elliptic-curve verification is handled in compiled code, and no third-party JWT libraries are required at runtime.
Monitoring and Failure Handling in Automated Builds
Automation only helps when failures are predictable and diagnosable.
Common issues include a missing C compiler, an unsupported CPython version, or duplicate module names across source files. Py2Native reports these at the CLI with enough context to locate the offending file. For example, duplicate module names are a known constraint: two source files cannot define modules with the same fully qualified name.
Use --base to control where source globbing starts:
uv run py2native build main.py *.py --base src/
This prevents the compiler from accidentally pulling in test helpers, build scripts, or unrelated Python files that happen to match a glob.
After a build, test the artifact in a clean environment. A smoke test can catch missing dependencies or platform assumptions before your users do:
uv run py2native build main.py *.py --wheel dist/
uv run --isolated --with dist/myapp-0.1.0.whl python smoketest.py
For Pro builds, inspect a license file before distribution:
uv run py2native show --public public.pem license.dat
That command displays the JWT claims and verifies the signature against the public key. It is a fast pre-release check that the license is valid and the expected issuer and audience are present.
Best Practices for Automated Python Binary Distribution
- Keep source files modular and avoid duplicate module names across files. This is a Py2Native constraint and also makes builds more predictable.
- Choose the right output mode. Use
--libraryfor reusable packages,--embedfor standalone applications, and--wheelfor package-index distribution. - Use the Pro plugin’s string compression and JWT license verification when distribution control matters beyond code hiding.
- Store private keys in CI/CD secrets. Never embed the private key in source or pass it into a build. Only the public key is baked into the executable.
- Update Py2Native and its dependencies regularly. The core depends on Cython, setuptools, and
uv; keeping them current brings performance and compatibility fixes. - Maintain a reproducible environment with
uv.lock. Document the exact build command and source layout so any developer or CI runner can recreate the artifact.
FAQ
Can Py2Native compile any Python code to a native binary?
Py2Native compiles your custom Python code into native machine code using Cython under the hood, but it hides all Cython-specific syntax and steps. Third-party libraries remain as Python source and are not compiled, but they work as-is with minimal configuration. Some constraints apply, such as no duplicate module names across source files.
How does Py2Native handle dependencies in the compiled binary?
Py2Native does not bundle third-party dependencies into the native binary by default. Instead, you can use --embed <dir> to create a uv-managed deployment directory that includes the compiled binary and all required dependencies, making it self-contained. Alternatively, --wheel <dir> creates a standard wheel that can be installed with a package manager, pulling dependencies from PyPI.
Is Py2Native suitable for commercial software distribution?
Yes. The open-source core is MIT licensed, so you can compile and distribute binaries freely. For enhanced protection and license management, the Pro plugin adds JWT-based license verification with elliptic-curve signatures, string compression, and related tooling. The Pro plugin is proprietary and requires a license.
Can I automate Py2Native builds in my CI/CD pipeline?
Absolutely. Py2Native is a command-line tool that fits into any CI/CD system. Use uv run py2native build in pipeline scripts to compile on every commit or release. The Pro plugin’s key generation and signing commands can also be automated, with private keys stored securely in CI secrets.
Conclusion: Automate Your Way to Protected Python Binaries
Py2Native removes the manual grind from Python binary distribution. One uv run py2native build command compiles, packages, and deploys native binaries from plain Python, with no hand-written Cython or C extension work. The time saved is immediate; the security gain is shipping compiled code instead of readable source.
Start with the open-source compiler at py2native.dev. For license enforcement and extra protection, add the Pro plugin and bake JWT verification into your builds.
uv run py2native build main.py
Compile once, automate forever.
Related posts
- Py2Native vs PyInstaller: Which Python to EXE Compiler Protects Your Code?
- How to Hide Python Code from Users: A Practical Guide
- Automate Python Code Protection in Your CI/CD Pipeline