Py2Native Compile custom Python into native machine code to protect proprietary code

← All posts

2026-09-30

Compile Python to EXE Without Exposing Source Code

Compile Python to EXE Without Exposing Source Code
python compiler cython code protection open source

Compile Python to EXE Without Exposing Source Code

If you ship a Python desktop app or CLI, you have probably hit the same wall: your main.py and helper modules are sitting in the install directory, readable by anyone who opens the folder. Packaging tools such as PyInstaller can bundle your script and interpreter into a .exe, but they do not hide the source code. The .pyc files and embedded strings can still be recovered by a determined user.

Py2Native takes a different route. It compiles your custom Python into native machine code, so the shipped executable contains compiled objects instead of readable .py files. You write plain Python; Py2Native handles the Cython and C toolchain steps behind the scenes.

In this how-to, you will compile a Python project into a native executable, verify that the source is not exposed, and optionally add JWT license verification with Py2Native Pro.

Prerequisites

Before you start, make sure you have:

  • CPython 3.11–3.15 installed, including free-threaded 3.14t and 3.15t
  • A C compiler and linker: MSVC on Windows, GCC on Linux, or Clang on macOS
  • An x86-64 or ARM64 CPU
  • Internet access to download Python distributions and libraries
  • The uv package manager installed
  • For optional license verification: the py2nativepro package and a keypair

All Py2Native commands in this article use uv run py2native. You will not use pip.

Step-by-Step: Compile Your Python Code to a Native Executable

Step 1: Create a simple Python project

Start with a small project that has a main module and a helper module. This makes the build command more realistic than a single-file example.

uv init native-example
cd native-example

Create two Python files:

# helpers.py
def format_greeting(name: str) -> str:
    return f"Hello, {name}!"
# main.py
from helpers import format_greeting

if __name__ == "__main__":
    print(format_greeting("Py2Native"))

Step 2: Add Py2Native with uv

Add the open-source Py2Native core to the project:

uv add py2native

This updates your pyproject.toml and makes the py2native CLI available through uv run.

Step 3: Run the build command

Compile the project with the glob pattern that includes all of your Python sources:

uv run py2native build main.py *.py

The first positional argument is the main module. The remaining arguments are the source files. Py2Native expands the glob, compiles the sources to native code, and links the resulting executable.

Step 4: Inspect the output

After the build completes, look at the output directory. On Windows, the executable is a .exe; on Linux and macOS, it is an ELF or Mach-O binary. The original .py files are not copied into the build output. Your application logic now lives in compiled native code, not in readable text files.

Step 5: Build a shared library instead of an executable

For importable packages, use --library:

uv run py2native build main.py *.py --library

This creates a shared library: .pyd on Windows, .so on Linux, or .dylib on macOS. In library mode, Py2Native does not compile __init__.py because it is a namespace marker. The wheel builder generates the required __init__.py meta-path finder and __main__.py entry point when you create a wheel.

Step 6: Create a wheel for distribution

If you want to distribute the compiled package, pair --library with --wheel:

uv run py2native build main.py *.py --library --wheel dist

The resulting PEP 427 wheel contains the compiled shared library, an __init__.py that redirects imports to the native library, and a __main__.py that lets users run python -m your_package.

Step 7: Embed a Python runtime for self-contained deployment

To ship a directory that includes its own Python runtime, use --embed:

uv run py2native build main.py *.py --embed deploy

Py2Native creates a uv-managed deployment directory. The compiled code and the embedded runtime go into that directory, giving you a self-contained target that does not depend on the end user having Python installed.

Verifying the Compilation Worked

Run the generated executable

Run the binary from the build output directory. The build output will show the exact path. The command should behave exactly like your original script:

Hello, Py2Native!

Confirm the source files are not present

In the build output directory, list the installed files:

find build -name "*.py"

You should not see your original main.py or helpers.py in the executable output. The Python source files are not shipped.

Check the binary for readable source text

Use strings to look for application-specific strings that would appear in raw Python source:

strings build/your_binary | grep "format_greeting"

Unlike a bundled .pyc file, the original Python source lines are not present as plain text in the compiled binary.

Verify library mode imports correctly

If you built a wheel or shared library, import the package in a fresh Python environment and call its entry point. The generated __init__.py bridges Python’s import system to the compiled native library.

Troubleshooting Common Issues

Missing compiler

If the build fails with a message about a missing compiler or linker, make sure your platform toolchain is installed and on your PATH:

cl          # Windows MSVC
gcc --version   # Linux GCC
clang --version # macOS Clang

On Windows, install the Visual Studio Build Tools with the C++ workload. On Linux, install build-essential. On macOS, install the Xcode Command Line Tools.

Module name conflicts

Py2Native requires unique module names across source files. If two files define the same module name, rename one. The compiler cannot disambiguate identical module names.

Monkey-patching is still possible

Py2Native compiles only your custom Python code. Third-party libraries remain as Python packages, so monkey-patching those dependencies is still possible at runtime. This is an intentional trade-off: your proprietary application logic is protected, while third-party libraries remain modifiable.

LGPL compliance

Similarly, LGPL libraries remain replaceable by the end user. Py2Native does not compile third-party dependencies into the native binary, so you can comply with LGPL requirements.

License verification errors

If the Pro license check fails, confirm that the license file was signed with the matching private key and that the public key passed to the build matches the key used for signing. Use the Pro CLI show command to inspect the JWT claims.

Adding License Verification with Py2Native Pro (Optional)

Py2Native Pro adds EC P-256 and JWT-based license verification to the build. The core compiler is open source under the MIT license; the Pro plugin is a commercial add-on that gives you signed license checks inside the compiled executable.

First, add the Pro plugin:

uv add py2nativepro

Generate an EC P-256 keypair:

uv run py2native keygen private.pem public.pem

Sign a JWT payload with your private key. The payload comes from a JSON file containing the claims you want to issue:

uv run py2native sign --private private.pem payload.json license.dat

You can inspect the resulting license at any time:

uv run py2native show --public public.pem license.dat

To enforce the license inside your application, add a .pxd declaration that tells Py2Native’s native bootstrap the signature of the verification function:

# license_verify.pxd
cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)

Then call the compiled verifier from your application code:

# main.py
from _p2n_bootstrap cimport _runtime_verify_es256_jwt

def require_valid_license(licenseString: str) -> None:
    license = _runtime_verify_es256_jwt(
        licenseString,
        expected_iss="RSJ Software GmbH",
        expected_aud="TimestampGIT",
    )
    if not license:
        raise SystemExit("License verification failed")

Build with the license and public key flags:

uv run py2native build main.py *.py --license license.dat --public public.pem

Py2Native Pro bakes the signature verification logic and the public key into the executable. Only the public key is stored in the binary; the private key never leaves your signing environment. The elliptic-key verification is handled in compiled code within Py2Native Pro, not through third-party libraries.

FAQ

Can I compile Python to EXE without exposing source code?

Yes. Py2Native compiles your Python code into native machine code, so the original .py source is not shipped. The resulting executable contains compiled objects, not readable source files.

Does Py2Native work with third-party libraries?

Yes. Third-party libraries are left unmodified and work as-is. Py2Native compiles only your custom Python code. Dependencies remain as Python packages.

What is the difference between Py2Native and PyInstaller?

PyInstaller bundles your Python scripts and interpreter into an executable, but the source can still be extracted from the bundle. Py2Native compiles your code to native machine code, making reverse engineering substantially harder.

Is Py2Native free to use?

The core compiler is open source under the MIT license. A commercial Pro plugin adds JWT license verification and signing features on top of the core compiler.

Keep Your Python Source Private

Py2Native gives you a practical, zero-config path from plain Python to a native executable. You do not need to write Cython by hand, maintain .pyx files for your application, or learn C toolchain flags. The hard way is manually running Cython and linking a C extension yourself; Py2Native automates that pipeline and keeps your source out of the shipped artifact.

Start with the command that protects your project:

uv run py2native build main.py *.py

For license enforcement and commercial distribution, explore Py2Native Pro pricing. To learn more about the compiler itself, visit Py2Native.

Related posts

EU label: AI-generated content