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

← All posts

2026-09-06

Protect Python Source Code Without Rewriting Your Code

Protect Python Source Code Without Rewriting Your Code
python compiler cython code protection open source

Protect Python Source Code Without Rewriting Your Code

When you ship Python software, you usually ship .py files. That means the custom pricing algorithm, validation logic, or business rules you spent months building can be opened, read, and modified by anyone with a text editor.

This guide covers the exact task: take an existing plain-Python project and compile your custom code into native machine code so you can distribute a compiled binary instead of readable source. By the end, you will have:

  • A native executable or shared library built from your existing Python files.
  • A wheel or self-contained deployment directory for distribution.
  • Optional Pro Edition license verification using JWT and a baked-in public key.

The hard way is raw Cython or hand-written C extensions: maintain .pyx files, run your own cythonize step, and manage platform-specific build flags. Py2Native hides all of that. You write plain Python, and one uv run py2native build command handles translation to C, C compilation, and linking. Under the hood it uses Cython, but you do not need Cython syntax, .pyx files, or a manual build pipeline.

Prerequisites

Before building, make sure your machine has:

  • CPython 3.11–3.15 installed, including the free-threaded variants such as 3.14t or 3.15t.
  • A platform C compiler/linker:
    • MSVC on Windows
    • GCC on Linux
    • Clang on macOS
  • Internet access so Py2Native can download Python distributions and dependencies when needed.
  • A Python project containing only plain Python. You do not need to rewrite any of it in Cython or C.

If you are unsure whether a compiler is available, check it before building:

gcc --version

On Windows, use a Developer Command Prompt so the MSVC tools are on your PATH.

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

The examples below assume a simple project structure:

myproprietaryapp/
├── main.py
├── pricing.py
└── customer.py

Your main.py might look like this:

from pricing import calculate_price
from customer import load_customer

def main():
    customer = load_customer()
    price = calculate_price(customer)
    print(f"Final price: {price}")

if __name__ == "__main__":
    main()

The logic in pricing.py and customer.py is the proprietary code you want to protect.

Step 1: Add Py2Native with uv

From your project directory, add Py2Native to the environment that uv manages:

uv add --dev py2native

All build commands below start with uv run py2native so uv resolves the correct Python interpreter and dependencies. You do not use pip here.

Step 2: Confirm your sources are plain Python

Py2Native expects the entry point and source files exactly as you normally write them. There is no step to convert files into .pyx or .pxd files for the core compilation workflow.

Step 3: Run the build command

To build a native executable from main.py and all Python modules in the project, run:

uv run py2native build main.py *.py

Py2Native expands the glob, generates C from your Python files, compiles the C, and links an executable. The same command works whether you list files individually or use shell globs.

Step 4: Understand the output

Once the build finishes, Py2Native writes the native executable to the build directory. On Linux this is an ELF binary, on Windows an executable, and on macOS a Mach-O binary.

Test it from the build output directory:

./build/main

Adjust the filename if your project uses a different output name.

Step 5: Build a wheel or an embedded deployment

For distribution, you usually want a wheel or a self-contained deployment directory instead of a bare executable.

To create a wheel containing the compiled extension:

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

To create a uv-managed deployment directory:

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

If your project is a package with multiple modules, use --library to build a shared library instead of an executable:

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

The generated library-mode wheel contains a single compiled shared object plus two required bridge files:

  • __init__.py — redirects Python imports to the native library.
  • __main__.py — enables python -m mypackage.

This means your package still works through the normal Python import system, but your custom module source is no longer distributed as plain text.

Step 6: Test the compiled binary

Run the compiled executable and compare its output with the original Python script:

python main.py
./build/main

The behavior should be identical. If your application accepts arguments or environment variables, test those paths as well.

Verifying the Protection

After the build, confirm that the output is actually native code.

Check the file type on Linux or macOS:

file ./build/main

On Windows, confirm that you have an executable rather than a .py file.

You can also check whether your Python source is still visible as plain text:

strings ./build/main | grep -i "def calculate_price" || echo "No plaintext source found"

This is not a formal security audit, but it is a quick sanity check. The goal is to ensure you are no longer shipping a directory of readable .py files.

For library mode, inspect the wheel contents:

unzip -l dist/*.whl

Look for the compiled .so, .pyd, or .dylib file plus __init__.py and __main__.py. Your original .py source modules should not appear in the wheel.

Adding License Verification with Py2Native Pro

The open-source core of Py2Native compiles your code. Py2Native Pro adds JWT-based license verification so you can control who runs the compiled binary.

The Pro plugin is packaged as py2nativepro. In a Pro environment, add it to the same uv-managed project:

uv add --dev py2nativepro

Step 1: Generate a keypair

Run the Pro keygen command to create an EC P-256 keypair:

uv run py2native keygen private.pem public.pem

Keep private.pem on your build machine. It signs licenses. Do not ship it.

Step 2: Sign a license

Create a JWT payload for your customer, then sign it with the private key:

uv run py2native sign --private private.pem \
  '{"sub":"customer-123","iat":1760000000,"exp":1893456000}' \
  license.dat

Replace iat and exp with the issue time and expiry time you want for the license.

You can inspect the signed license:

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

Step 3: Add the license gate to your Python code

Py2Native Pro bakes the verification logic and public key into the executable. To call that logic from your code, add a .pxd declaration file that imports the verifier from the generated bootstrap module:

# license_check.pxd
from _p2n_bootstrap cimport _runtime_verify_es256_jwt
cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)

Then call the verifier at runtime from your Python source:

from _p2n_bootstrap import _runtime_verify_es256_jwt

licenseString = open("license.dat", "r", encoding="utf-8").read().strip()

license = _runtime_verify_es256_jwt(licenseString, expected_iss="RSJ Software GmbH", expected_aud="TimestampGIT")

if not license:
    raise SystemExit("Invalid or missing license")

# Your proprietary code runs only after license verification succeeds.
def main():
    ...

This is not a general-purpose Cython workflow. The .pxd file is the license-gate contract for Py2Native Pro. The rest of your project remains plain Python.

Step 4: Build with the license and public key

Run the build with the --license and --public flags:

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

The Pro plugin checks the license as part of the build and bakes the signature verification code and public key into the executable. Only the public key is stored in the binary. The elliptic key verification is handled with compiled code inside Py2Native Pro, so you do not depend on third-party verification libraries at runtime.

Troubleshooting Common Issues

Compiler not found

If the build fails with a compiler or linker error, confirm that your platform toolchain is installed and available on your PATH.

On Linux:

gcc --version

On macOS:

clang --version

On Windows, run the build from a Developer Command Prompt or confirm that MSVC is available.

Identical module names across source files

Py2Native does not support modules with identical names across source files. If you have two files named utils.py in different directories, rename one or convert the project into explicit package-qualified module names.

Monkey-patching is still possible

Compiling your custom code to native code does not freeze third-party libraries. Third-party Python libraries remain as Python source, so an end user can still modify those libraries. Py2Native protects your custom code; it does not claim to protect third-party code that stays in source form.

LGPL libraries remain replaceable

If you distribute LGPL libraries, those libraries remain replaceable by the end user for compliance. This is separate from your compiled custom modules.

Windows build failures

Check that the correct Windows SDK is installed and that the Py2Native Windows platform plugin is loaded. Building from a normal PowerShell session without MSVC tools is a common source of errors.

FAQ

Does Py2Native require me to write Cython code or .pyx files?

No. Py2Native uses Cython under the hood but completely hides it. You write plain Python, and Py2Native handles the translation to C and compilation automatically.

Can I compile a Python package with multiple modules into a single native library?

Yes. Use the --library flag to create a shared library containing all your modules. The generated wheel includes an __init__.py that redirects imports to the native library, so your package works as usual.

How does license verification work in Py2Native Pro?

Py2Native Pro uses JWT with ES256 signatures. You generate a keypair, sign a license with the private key, and build your executable with the license and public key. The verification code and public key are baked into the binary. At runtime, your code calls _runtime_verify_es256_jwt to validate the license.

Will my compiled binary be completely immune to reverse engineering?

No protection is absolute. Compiling to native code makes it significantly harder to read and modify your logic compared with Python bytecode or source. However, determined attackers with reverse engineering skills may still analyze a native binary. Py2Native raises the bar substantially.

Conclusion

Py2Native compiles your existing Python code into native binaries with one command. You do not rewrite modules in C, maintain Cython translation files, or hand-configure compiler flags for every platform. The open-source core handles the build pipeline, and the Pro plugin adds JWT license verification when you need to control distribution.

Start small: run uv run py2native build main.py *.py on a proprietary module you already have, then inspect the build output. Once that works, add --wheel or --embed for distribution, and consider the Pro Edition if you need signed license enforcement.

Try Py2Native on your project and see how quickly you can move from readable .py files to a compiled native binary.

Related posts

EU label: AI-generated content