How to Hide Python Code from Users: A Practical Guide
Shipping Python code usually means shipping the source. A pricing.py or auth.py file sitting next to your executable can be opened, copied, and understood in minutes. If your software contains a proprietary algorithm, a sensitive data pipeline, or a client-side license check, that exposure is a real business problem.
In this guide, you’ll hide your Python source by compiling it into native machine code with Py2Native. You’ll start with only plain Python files and end with a native executable, wheel, or embedded deployment directory. No hand-written C extensions, no manual cythonize steps, and no .pyx files for normal builds.
If you’ve looked at the manual path—writing Cython stubs, configuring C compilers, and maintaining extension modules—Py2Native is the practical alternative: one command does the work. For a deeper look at the compilation mechanics, see the Python Native Compilation Tutorial.
Why Hide Python Code?
Python’s default artifacts are not secret. A .py file is readable source. A .pyc file can be decompiled with widely available tools. Obfuscation can slow an attacker down, but it does not stop someone who is determined to recover readable logic.
Native compilation changes the equation. Your Python code becomes machine code, so the original source is no longer shipped inside the delivered binary. Third-party libraries remain as ordinary Python source, which Py2Native intentionally leaves unmodified, but your custom logic is compiled into a native shared object or executable.
Py2Native manages the entire Cython-to-C-to-native pipeline behind a single command. You write Python, run uv run py2native build, and get a compiled artifact. The compiler itself is open source under the MIT license, and a commercial Pro plugin adds signed license verification and string compression on top.
Prerequisites
To follow along, you need:
- Python 3.11 through 3.15, including free-threaded 3.14t and 3.15t builds
- uv as the package manager and command runner
- A platform C compiler:
- MSVC on Windows
- GCC on Linux
- Clang on macOS
- A 64-bit CPU: x86-64 or ARM64
- Internet access during the first build, because Py2Native downloads Python distributions and libraries as needed
Create a small project and add Py2Native:
uv init py2native-demo
cd py2native-demo
uv add py2native
Use uv run for every Py2Native command in this guide.
Step-by-Step: Compile Your Python Code to a Native Binary
Step 1: Organize your project
Start with a normal Python project. Keep a clear entry point and separate your custom modules:
py2native-demo/
├── main.py
└── pricing.py
Example source:
# pricing.py
def apply_discount(price: float, customer_tier: str) -> float:
if customer_tier == "gold":
return price * 0.80
return price
# main.py
from pricing import apply_discount
def main():
total = apply_discount(100.0, "gold")
print(f"Total: {total:.2f}")
if __name__ == "__main__":
main()
There is nothing special here. Py2Native treats these files as plain Python, then compiles them into native code.
Step 2: Build a native executable
Run:
uv run py2native build main.py *.py
Py2Native expands the glob, compiles every listed source file, and links the result into a native executable. The command prints the output path when it completes.
This is the zero-config path. There are no .pyx files to write, no C compiler flags to tune, and no extension modules to register. The compiler invokes Cython under the hood but hides that machinery from you.
Step 3: Create a wheel or embedded directory
For distribution, Py2Native can produce two artifacts.
Create a wheel:
uv run py2native build main.py *.py --wheel dist/wheel
This creates a PEP 427 wheel containing the compiled .pyd or .so file. Python code outside the wheel cannot read your original source because it is no longer present.
Create an embedded deployment directory:
uv run py2native build main.py *.py --embed dist/embed
This builds a self-contained deployment directory managed by uv. It includes the compiled binary and the runtime pieces needed to execute it.
Step 4: Compile an importable package with --library
If you want to ship a package that users can import, use library mode:
uv run py2native build --library main.py mypkg/*.py
The output wheel contains:
- A single compiled shared library with all of your modules
- An
__init__.pythat redirectsimport mypkg.submoduleto the shared library - A
__main__.pythat enablespython -m mypkg
Both __init__.py and __main__.py are required. The first bridges Python’s import system to the native library. The second provides a runnable entry point. Py2Native generates these files for you.
Step 5: Add Pro license verification
For commercial products, the Pro plugin adds signed JWT license verification. The verification code and the public key are baked into the executable, so your license check is not a plain Python function that an attacker can patch by editing a source file.
First, generate a keypair and a signed license:
uv run py2native keygen private.pem public.pem
uv run py2native sign --private private.pem '{"iss":"RSJ Software GmbH","aud":"TimestampGIT"}' license.dat
uv run py2native show --public public.pem license.dat
Keep the private key on your build machine only. The public key is what ends up embedded in the compiled application.
Next, add a small .pxd declaration. This is the one place where Py2Native’s Pro workflow asks for a declaration file, because the Pro runtime must expose its compiled verification function to your Python code:
# license_check.pxd
from _p2n_bootstrap cimport _runtime_verify_es256_jwt
cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)
Create a Python module that wraps this declared runtime function:
# license_check.py
from license_check cimport _runtime_verify_es256_jwt
def enforce_license(licenseString: str) -> None:
license = _runtime_verify_es256_jwt(
licenseString,
expected_iss="RSJ Software GmbH",
expected_aud="TimestampGIT",
)
if not license:
raise RuntimeError("Invalid license")
Call that check from your entry point:
# main.py
import sys
from license_check import enforce_license
def main():
if len(sys.argv) < 2:
print("Usage: main.py <license.dat>")
sys.exit(1)
with open(sys.argv[1]) as f:
licenseString = f.read().strip()
enforce_license(licenseString)
print("Starting protected application...")
if __name__ == "__main__":
main()
The verification itself happens in compiled code. You are not writing C or Cython logic; you are calling the runtime entry point that the Pro plugin embeds into the binary.
Step 6: Build with the license and public key
Compile the Pro-protected application:
uv run py2native build main.py *.py --license license.dat --public public.pem
Py2Native stores only the public key in the executable. The elliptic key verification is performed by compiled code in the Pro plugin, not by a third-party Python library.
Step 7: Test the resulting binary
Run the compiled executable exactly as you would run the Python main module:
./main license.dat
If the license is valid, the application starts. If the license is missing or invalid, the runtime check fails and your code raises the intended error.
How to Confirm It Worked
Run the compiled binary and compare its behavior with the original Python script. It should accept the same inputs and produce the same outputs.
Then inspect the output directory. For executable and wheel builds, your .py source files should not be present. In library mode, only the generated __init__.py remains, and it contains import machinery—not your application source.
You can also check the binary itself:
strings dist/wheel/*.so | grep -i "apply_discount"
If your function name appears as a string inside the compiled artifact, that is not automatically a source leak, but your readable Python source code should not appear. A hex editor or strings dump should not show the original implementation.
For Pro license verification, test both paths:
./main valid-license.dat
./main tampered-license.dat
The valid license should start the application. The tampered license should fail before any protected logic runs.
Troubleshooting Common Issues
Missing C compiler. Py2Native needs a working platform toolchain. Install MSVC Build Tools on Windows, GCC on Linux, or the Xcode Command Line Tools on macOS.
Module name conflicts. Do not define two source files with the same module name. Py2Native compiles all sources into one build, so duplicate module names cause conflicts.
Third-party libraries still contain Python source. Py2Native compiles your custom code. Third-party dependencies remain as ordinary Python packages and are imported normally at runtime. If you also need to protect those, you will need a different strategy for those dependencies.
Monkey-patching is still possible. Native code can still be monkey-patched at runtime by Python callers. Py2Native protects your source from direct reading, but it does not make the Python object model disappear.
License verification fails.
Check that the .pxd declaration is included, the license file passed to --license is the one created with sign, and the expected issuer and audience match the signed JWT exactly:
iss: RSJ Software GmbH
aud: TimestampGIT
FAQ
Q: Can users recover my Python source code from the compiled binary?
A: No. The binary contains native machine code, not Python bytecode or source. Reverse engineering is theoretically possible, but it is significantly harder than decompiling .pyc files or reading obfuscated Python.
Q: Does Py2Native work with third-party Python libraries?
A: Yes. Third-party libraries remain as Python source and are imported normally at runtime. Only your custom Python code is compiled to native code.
Q: Is Py2Native free to use?
A: The core compiler is open source under the MIT license and free to use. A commercial Pro plugin adds JWT license verification and string compression.
Q: Can I compile my code into a single executable file?
A: Yes. By default, Py2Native produces a native executable. You can also create a self-contained deployment directory with --embed or a distributable wheel with --wheel.
Conclusion
Py2Native is the practical way to hide Python code from users without leaving Python development. You write plain Python, run uv run py2native build, and receive a compiled native artifact. The open-source core covers the complete compilation pipeline, embed directory creation, and wheel building. The Pro plugin adds signed JWT license verification and string compression when you need stronger commercial protection.
If you want to automate code protection, see Automate Python Code Protection in Your CI/CD Pipeline. If you want to avoid .pyx files entirely, see Cython Alternative: Compile Python Without Writing .pyx Files.
Get started with Py2Native and turn your next Python release into a binary your users cannot simply open and read.