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

← All posts

2026-10-09

Compile Python to an Executable Without Cython Syntax

Compile Python to an Executable Without Cython Syntax
python compiler cython code protection open source

Compile Python to an Executable Without Cython Syntax

You have a working Python application. Part of it is your competitive advantage — pricing logic, a scoring engine, a licensing module — and you’d like to ship it as a native binary instead of a folder of readable .py files. So you search for how to compile Python to an executable, and every guide starts the same way: learn .pyx syntax, write a setup.py with cythonize, declare your types, fight the C compiler, repeat.

That’s one way to do it. It is not the only way, and for most teams shipping proprietary software it is the wrong trade: you end up maintaining two codebases (Python and Cython) and a build pipeline that only one person on the team understands.

This tutorial takes the other path. You will keep your plain Python source exactly as it is and still get native machine code out of it — no Cython syntax, no handwritten C, no manual transpilation step. By the end you’ll have a compiled binary (or wheel) you can hand to a customer, plus a verification recipe to prove the proprietary logic is no longer sitting there as source.

Why You Don’t Need to Learn Cython Syntax to Compile Python

The confusion comes from conflating the transpiler with the tooling you have to drive. Cython is a Python-to-C transpiler. Something has to turn your code into C, then into objects, then link it into a binary. Tools like Py2Native use Cython for exactly that step — and then hide it completely.

What that means in practice:

  • You write plain .py. No .pyx, no .pxd for your normal modules, no type annotations added just to satisfy the compiler.
  • There is no cythonize call in your workflow and no setup.py to hand-tune.
  • There are no C extensions to write or maintain.
  • Third-party libraries are left untouched. They stay as Python source — which is why they keep working without being rewritten.

The alternative — raw Cython, hand-written C, or a bespoke build configuration — is powerful if you need to optimize numeric kernels by hand. As a code protection strategy it’s a lot of work for the same end result: machine code the customer can’t read.

The hard way The zero-config way
Source you write Python + .pyx/.pxd + C glue Plain Python
Transpilation step You invoke cythonize manually Handled for you
Build config Per-module, per-platform One command
Third-party libs Often need rewrites or wrappers Unmodified, work as-is

In this tutorial you’ll go from an existing Python project to a compiled, protected native artifact without editing your source.

Prerequisites for Compiling Plain Python to a Native Binary

Before the first build, confirm you have:

  • CPython 3.11–3.15, including the free-threaded 3.14t and 3.15t builds.
  • A platform C compiler and linker: MSVC on Windows, GCC on Linux, Clang on macOS. Py2Native compiles real native code, so a real toolchain is required.
  • A supported OS and CPU: Windows 8+, manylinux2014 or musl-based Linux, or macOS — on x86-64 or ARM64.
  • uv >= 0.11.8 installed. Every command in this article starts with uv run py2native. There is no pip step anywhere.
  • Internet access during the build, because Python distributions and libraries are downloaded on the fly.

If you have a C compiler and uv, you have everything else.

Step-by-Step: Compile Your Python Project Without Cython Syntax

Step 1 — Install Py2Native

Add it to your project, or install it as a standalone tool if you’ll build from multiple repos:

uv add py2native
# or, for a global tool install:
uv tool install py2native

Confirm the CLI is reachable:

uv run py2native --help

You’ll see the build command along with the Pro-only keygen, sign, and show commands (those appear when the Pro plugin is present).

Step 2 — Prepare Your Project

Nothing to rewrite. Point the builder at your existing files, and keep two constraints in mind:

  • __init__.py is skipped during compilation — it’s a namespace marker, not compilable code.
  • No two source files may share the same module name. If you have core/utils.py and plugins/utils.py, rename or restructure so the module names are unique across the build.

Everything else — your imports, your third-party dependencies, your package layout — stays where it is.

Step 3 — Run the Build

The core invocation is short. The first positional argument is your main module; the rest are the sources to compile:

uv run py2native build main.py *.py

Under the hood, build.py expands the glob patterns, the plugin manager runs any build-time hooks, compiler.py generates a bootstrap.pyx from its templates, invokes Cython, compiles every resulting .c file with platform-appropriate flags, and links the result into a native binary. You never see any of those steps in your terminal beyond progress output.

If your sources live below the project root, glob from there:

uv run py2native build main.py *.py --base src

For a GUI or background app where you don’t want an attached console window, add --no-console:

uv run py2native build main.py *.py --no-console

Step 4 — Choose Your Output Format

You have four deployment shapes, and you can combine them:

# Native executable (default)
uv run py2native build main.py *.py

# uv-managed deployment directory — Python runtime + your binary, ready to ship
uv run py2native build main.py *.py --embed ./dist

# PEP 427 wheel containing the compiled .pyd/.so
uv run py2native build main.py *.py --wheel ./dist

# Shared library instead of an executable
uv run py2native build main.py *.py --library --wheel ./dist

Pick based on how the customer consumes your software: an executable for an end-user desktop tool, a wheel for a Python-facing library, a shared library for embedding in a larger system.

Step 5 — Understand Library Mode Before You Ship It

--library produces a shared library — .pyd on Windows, .so on Linux, .dylib on macOS — containing all your compiled modules. The wheel that wraps it also contains two generated files:

  • __init__.py — a meta-path finder that redirects import mypkg.submodule to the compiled shared library.
  • __main__.py — the entry point that makes python -m mypkg work.

Both are required. __init__.py is what bridges Python’s import machinery to your native code; __main__.py is what makes the package runnable. Delete either one and you’ll get import errors or a package that does nothing when invoked. (Note that __main__.py is excluded from your source list in library mode — the wheel builder generates it.)

Step 6 — Optional: Add Pro License Verification

The community edition is MIT-licensed and compiles your code. The Pro plugin (py2nativepro) adds signed-license enforcement: JWT verification, string compression, and the keygen/sign/show CLI commands.

The first step is generating a keypair and signing a license payload. Only the public key ends up in your executable:

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

Next, include the .pxd file that declares the runtime verification entry point so your code can call it:

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

Then call the verification logic with the license key in your own code, in a module that Py2Native compiles:

# license_check.py — compiled by Py2Native, no Cython syntax required
from _p2n_bootstrap cimport _runtime_verify_es256_jwt


def check_license(licenseString):
    license = _runtime_verify_es256_jwt(
        licenseString,
        expected_iss="RSJ Software GmbH",
        expected_aud="TimestampGIT",
    )
    if license is None:
        raise SystemExit("Invalid or missing license.")
    return license

Finally, build with the license and public-key files:

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

The plugin bakes the signature verification code and the public key into the executable. Verification is handled by compiled code inside the product — no third-party crypto library is linked in, and the private key never leaves your machine.

Step 7 — Distribute

Ship the executable, the --embed deployment directory, or the wheel. Third-party libraries remain unmodified and behave as they always did, so there’s no per-dependency patching pass before release.

Two constraints worth knowing before you promise anything to a customer: monkey-patching is still possible (third-party libraries stay as Python source), and LGPL libraries remain replaceable by the end user for compliance purposes.

How to Confirm Your Python Was Compiled to Native Code

Verification takes about a minute:

1. Check the artifact types. In your output directory you should see .pyd (Windows), .so (Linux), or .dylib (macOS) files — not the .py sources you passed in:

ls -la dist/
file dist/mypkg/*.so

2. Run it and compare behavior.

./dist/mypkg
# or, for a library build:
python -m mypkg

Output should match your original script exactly.

3. Look for readable source. This is the whole point, so check it directly:

strings dist/mypkg | grep -i "def calculate_price"

Your proprietary logic shouldn’t appear as readable Python. You’re looking at machine code now, not a .py file someone can open in an editor.

4. For Pro builds, inspect the embedded claims.

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

This prints the JWT claims and verifies the signature against the public key — a quick way to confirm the license baked into the binary matches the key you signed with.

Troubleshooting Common Compilation Issues

  • “No compiler found” / linker errors. Install the platform toolchain: MSVC Build Tools on Windows, GCC on Linux, Xcode Command Line Tools on macOS.
  • Duplicate module names. If the build complains about two modules with the same name, rename or restructure your files so every compiled module name is unique.
  • Import errors in library mode. Check that the generated __init__.py and __main__.py are both present in the wheel. __init__.py is required for imports; __main__.py is required for python -m mypkg.
  • License verification failures. Confirm the JWT was signed with the private key matching the public key you passed to --public. A mismatched pair fails closed.
  • manylinux errors on Linux. auditwheel repair runs automatically during the build. If you still hit manylinux errors, check that your base image satisfies manylinux2014 or musl requirements.
  • A missing __init__.py in the output. That’s expected — it’s skipped during compilation rather than compiled.

What About Cython, Nuitka, and Other Approaches?

Raw Cython is excellent technology and genuinely the hard way to protect code: .pyx and .pxd syntax, a manual cythonize invocation, and per-platform C compiler configuration. You get full control and pay for it in maintenance.

Nuitka and similar tools can produce native output too, but they frequently require extensive build configuration and don’t always leave third-party libraries untouched — which is where “minimal configuration” promises tend to break down in real projects.

Py2Native’s trade is the opposite: plain Python in, native machine code out, third-party libraries untouched, and one command per build. Choose it when your goal is code protection — not when you’re hand-optimizing a numeric kernel. If you’re weighing protection strategies more broadly, see Python Obfuscation vs Native Compilation and Protect Python Code for Commercial Software Vendors.

FAQ

Do I need to write any Cython or C code to compile my Python to an executable? No. Py2Native uses Cython under the hood but hides it completely. You write plain Python — no .pyx/.pxd files, no manual cythonize step, no C extensions. Just run uv run py2native build main.py *.py and get a native binary.

Will my third-party Python libraries still work after compilation? Yes. Py2Native leaves third-party libraries unmodified, so they work as-is with minimal or no configuration. Only your custom Python code is compiled to native machine code.

How do I add license verification to my compiled Python app? Use the Pro plugin. Add the --license and --public flags to the build, include the .pxd file, and call _runtime_verify_es256_jwt(licenseString, expected_iss="RSJ Software GmbH", expected_aud="TimestampGIT") in your code. The plugin bakes the signature verification code and public key into the executable.

What output formats can Py2Native produce? Py2Native can produce a native executable, a shared library (.pyd/.so/.dylib) via --library, a PEP 427 wheel via --wheel, or a uv-managed deployment directory via --embed.

Conclusion

Compiling Python to an executable doesn’t require learning Cython syntax, and it doesn’t require rewriting your project into a hybrid Python/C codebase. Keep your .py files exactly as they are, run one build command, and ship machine code instead of readable source.

If you’re doing this repeatedly — or on more than one platform — the next step is putting that build into your pipeline; Automating Python-to-Native Builds in CI With Py2Native covers that. And if you need to hand a customer a build that only runs with a signed license, the Pro plugin’s keygen/sign/show flow is the shortest path there.

Start with the community edition, which is MIT-licensed, and add Pro when you need license enforcement.

Related posts

EU label: AI-generated content