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

← All posts

2026-10-05

Protect Python Code for Commercial Software Vendors

Protect Python Code for Commercial Software Vendors
python compiler cython code protection open source

Protect Python Code for Commercial Software Vendors

Maya shipped her first commercial product last spring: a desktop data-reconciliation tool for accounting teams, about 28,000 lines of Python, three years of weekends and evenings. It sells for €490 per seat. Two weeks after launch, a prospect asked for a “technical evaluation” copy. Maya sent it. A day later she found her license.py, pricing.py, and the entire project layout sitting in a public gist, comments and all.

That is the moment every independent software vendor hits eventually. Python’s readability — the thing that made you productive — becomes a liability the second someone pays you money for the result.

This article is a playbook for vendors in that position. Not a deep technical comparison of protection techniques (that’s here), but the practical question: how do you ship Python as a commercial product without handing over the recipe?

The Vendor’s Dilemma: Shipping Python Without Shipping Your Secrets

If you sell software, your Python source is simultaneously your product and your biggest liability. A .py file is a text file. Anyone with a text editor, a zip tool, or five minutes can read your business logic, your pricing rules, your API endpoints, and the exact checks your license code performs.

The business stakes are concrete:

  • Lost licensing revenue. The moment a customer can read the function that decides whether a license is valid, they can neutralize it. That’s not hypothetical piracy — it’s a Tuesday afternoon for a motivated engineer.
  • IP theft. Your differentiator is often a small, dense module: a scheduling algorithm, a pricing engine, a domain-specific parser. That’s the part worth copying.
  • Competitive exposure. Your architecture, your integrations, your roadmap hints — all visible to a competitor who buys one seat.
  • The awkwardness of asking customers not to look. “Please don’t inspect the source” is not a license term. It’s a wish.

So the core question for a commercial vendor isn’t “should I protect my code?” It’s “what does protection look like when I have a business to run and no time to become a C developer?”

That’s the gap this article fills.

Why Commercial Vendors Can’t Just Obfuscate or Freeze

Two approaches dominate the amateur toolkit, and both fall short for vendor-grade software.

Obfuscators rewrite your Python into something uglier. Variable names become _0x4f2a, strings get encoded, control flow gets mangled. But the output is still Python bytecode or still Python source — and the tools to reverse it are widely available and often free. Obfuscation is a speed bump. For a hobby script, fine. For €490-a-seat software, it’s a delay, not a defense.

Freezers (PyInstaller-style bundlers) package your app with the interpreter into one executable. Convenient, but the bytecode is inside, and extracting it is a well-documented process. Everyone who has ever run strings on a frozen binary knows how little stands between the archive and your source. Freezers also have a nasty habit of breaking with dynamic imports, plugins, and unusual third-party packages.

For an ISV, the threat model is broader than “a curious user”:

  • Paying customers who want to remove seat limits.
  • Competitors who want your implementation, not your binary.
  • Anyone with a shell, since strings, unzip, and a disassembler are standard equipment.

Native compilation is the vendor-grade answer. Instead of shipping Python that a machine interprets, you ship machine code that a CPU executes. There is no readable bytecode to extract because there is no Python in the artifact — only compiled logic.

Py2Native does exactly this: it compiles your custom Python into native machine code. It uses Cython under the hood, but hides it completely. You write plain Python. No .pyx files, no .pxd files you have to author by hand, no manual cythonize step, no C extensions to write or maintain. If you’ve been told that protecting Python means learning Cython first, that’s the long way around.

A Vendor’s Playbook: Compiling Your Commercial Python App with Py2Native

Here’s the workflow end to end. Assume your entry point is main.py and the rest of your code sits alongside it.

Step 1 — Install and run the compiler with uv

Py2Native runs through uv. You never need pip.

# one-off: run the compiler directly from the project
uv run py2native build main.py *.py --embed dist

--embed dist produces a uv-managed deployment directory: a native executable plus the runtime environment it needs. If your distribution model is a Python package rather than a standalone app, build a wheel instead:

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

The flags map to your distribution choice, not to your code:

You want to ship… Use
A standalone desktop/on-prem executable --embed <dir>
A pip-installable package with compiled internals --library --wheel <dir>
A windowed app with no console add --no-console
Sources under a subdirectory add --base <dir>

Step 2 — Understand what you get

In executable mode (--embed), you get a native binary. Your modules are compiled; there’s no .py tree to read.

In library mode (--library), you get a shared library — .pyd on Windows, .so on Linux, .dylib on macOS — containing all your compiled modules. The wheel wraps it with two small generated files:

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

Both are required. __init__.py bridges Python’s import system to the native library; __main__.py gives you a runnable entry point. Neither contains your logic — they’re plumbing.

Step 3 — Leave your dependencies alone

This is where most vendors expect pain and find none. Third-party Python libraries are not modified. They stay as Python source and work as-is, with minimal or no configuration. Only your custom code gets compiled.

That’s the right trade-off for a vendor: you’re protecting the code you wrote, not reimplementing pandas. No forks, no vendored patches, no build matrix per dependency.

Step 4 — Add license verification (Pro)

If you sell licenses, you want enforcement that ships inside the binary. That’s the Pro plugin’s job.

Generate a keypair:

uv run py2native keygen private.pem public.pem

Create the license payload, then sign it into a JWT:

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

Inspect or verify a license at any time:

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

Now build with the license requirement enabled:

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

Only the public key ends up stored in the executable. The elliptic-curve verification is handled by compiled code inside your binary — no third-party crypto libraries involved.

Step 5 — Call the verification logic in your code

Add a .pxd declaration so the compiler knows the verification entry point the Pro plugin injects:

# license_guard.pxd
from _p2n_bootstrap cimport _runtime_verify_es256_jwt

cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)

Then call it with the license string your app receives from the user:

# license_guard.pyx
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",
    )
    return bool(license)

Your plain-Python app code just imports the guard:

# main.py
from license_guard import check_license

token = read_license_file()          # wherever the customer's key lives
if not check_license(token):
    raise SystemExit("Invalid or expired license. Contact support.")

The plugin bakes the signature verification code and the public key into the executable, so the check itself isn’t something a customer can edit out with a text editor. For details on offline scenarios, see Offline License Verification in Compiled Python Desktop Apps.

Checklist: What to Verify Before You Ship

Run through this before your first paying customer touches the artifact.

  • Build target matches your distribution. Standalone desktop/on-prem → --embed. Package install → --library --wheel. Windowed app → add --no-console.
  • Platform requirements are met. CPython 3.11–3.15, including the free-threaded 3.14t and 3.15t builds. A platform C compiler/linker: MSVC on Windows, GCC on Linux, Clang on macOS. Windows 8+, manylinux2014 or musl Linux, or macOS. x86-64 or ARM64. The build itself needs internet access to download Python distributions and libraries.
  • No duplicate module names across your source files. This is a known constraint — utils.py in two different directories will collide. Rename one.
  • __init__.py is skipped during compilation. It’s a namespace marker, not compilable code. Don’t put logic there.
  • In library mode, __main__.py is excluded from sources — the wheel builder generates it.
  • Test the compiled artifact on a clean machine, not your dev box. A build that runs fine locally can fail on a customer’s machine when a runtime dependency is missing.

What to Avoid When Protecting Commercial Python Code

Don’t rely on obfuscation alone. It’s a speed bump, not a lock. If your revenue depends on the code staying unread, compile it. Full reasoning: Python Obfuscation vs Native Compilation.

Don’t hand-write C extensions or drive Cython manually. That’s the hard way: you’d author .pyx/.pxd files, wire up cythonize yourself, maintain C build flags per platform, and slow every feature to a crawl. Py2Native exists so you don’t. Write plain Python; get a native binary.

Don’t fork your dependencies. Vendors often assume compiling means rewriting their dependency stack. It doesn’t. Third-party libraries stay unmodified and work as-is.

Don’t forget license compliance. LGPL libraries must remain replaceable by the end user, and monkey-patching remains possible for third-party code. Plan your legal posture accordingly — that’s a real constraint, not a bug.

Don’t skip testing the compiled artifact. The most common “it broke after compiling” report is an environment problem, not a compiler problem.

FAQ

Do I need to know Cython to use Py2Native?

No. Py2Native hides Cython completely. You write plain Python, and the compiler generates the necessary Cython bootstrap and C code behind the scenes. There are no .pyx or .pxd files to write, no manual cythonize step, and no C extensions to maintain.

How does license verification work without exposing the public key?

The Pro plugin bakes the signature verification code and the public key directly into the compiled executable. You generate a keypair with uv run py2native keygen, sign a JWT with uv run py2native sign, and build with --license and --public. In your code, you call _runtime_verify_es256_jwt(token, expected_iss="RSJ Software GmbH", expected_aud="TimestampGIT") to validate the license at runtime. The elliptic key verification is handled with compiled code, so no third-party libraries are needed.

What about my third-party dependencies?

Third-party Python libraries are left unmodified and work as-is with minimal or no configuration. Only your custom code is compiled to native machine code. You don’t need to rewrite or fork your dependencies, and monkey-patching remains possible for third-party code.

Can I still use uv for packaging?

Yes. Py2Native is designed to work with uv. You run the compiler via uv run py2native build, and the --embed option creates a uv-managed deployment directory. The --wheel option creates a PEP 427 wheel with the compiled .pyd/.so files, which you can distribute through your normal Python packaging channels.

Conclusion: Ship with Confidence, Not Exposure

For a commercial vendor, the choice isn’t between “perfect protection” and “no protection.” It’s between shipping readable source and shipping something a customer has to work to reverse. Native compilation puts you firmly in the second category.

The playbook is short:

  1. Write plain Python — no Cython syntax, no manual cythonize, no C extensions.
  2. Compile with uv run py2native build main.py *.py --embed dist (or --library --wheel dist).
  3. Add the Pro plugin when you need signed-JWT license enforcement baked into the binary.

The core compiler is open source under the MIT license, so you can evaluate the whole pipeline before spending a cent. When license enforcement becomes a business requirement, the proprietary Pro plugin adds keygen, signing, verification, and string compression on top.

Start with the Community edition:

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

Then upgrade to Pro when your first enterprise customer asks what happens if they share their seat.

Related posts

EU label: AI-generated content