Skip to main content

PyEncode

PyEncode protects CPython applications on Windows, Linux, and macOS. It compiles Python modules to code objects, serializes them with marshal for the portable runtime or a tagged primitive codec for the native runtime, compresses them, and authenticated-encrypts them with AES-256-GCM. The generated runtime decrypts and executes code in memory; neither plaintext source nor plaintext bytecode is written to a temporary file.

Version 0.7.1 retains the PYE2/manifest-format-3 compatibility contract and the encrypted eligible statically spelled attribute names added at --obf-code 2. It makes the protected platform wheels self-contained for offline reusable-runtime builds. The initial exact native targets are standard CPython 3.14 on Windows x64 (cp314-win_amd64) and the publisher-built macOS Apple Silicon target (cp314-macosx_arm64). The independent hardening controls are:

  • --obf-code 0 decrypts each module once when it is imported. --obf-code 1 additionally puts the original code of module-level functions, methods, and lambdas in build-time authenticated capsules. The visible functions expose signature-preserving dispatch stubs while idle and restore their real CodeType only for a call. Nested definitions remain protected inside their encrypted parent and are sealed when that parent creates them. --obf-code 2 includes level 1 and transforms eligible statically spelled attribute loads, stores, and deletes into authenticated AES-GCM name capsules dispatched by the runtime protector. This matches the documented meaning of PyArmor's current --obf-code 2 option—hiding names in attribute chains—not its separate RFT, BCC, string-mixing, private/restrict, or anti-debugging features.
  • --runtime-backend python emits the portable Python source runtime. --runtime-backend bytecode emits the same runtime as five adjacent, sourceless .pyc files bound to the build interpreter. The protected-wheel --runtime-backend native flow copies one publisher-built, catalog-bound, immutable pyencode_runtime._runtime extension. Every application still gets a fresh master key, which is wrapped to the selected runtime_id and authorized by a signed build permit. Matching protected platform wheels contain the verified runtime pack and a protected offline permit capability, so ordinary native builds do not need external runtime/key paths or a network connection. Application modules remain encrypted .pye artifacts.
  • --native-module is a separate application AOT option. Use it only when selected application modules themselves should become Cython/Zig .pyd extensions; it is not implied by a native runtime.

The reusable native backend always uses --obf-code 2; it rejects levels 0 and 1 instead of weakening or rebuilding the runtime. Portable Python and bytecode remain explicit, separate backends. Native mode never falls back to either one when an asset, target, or permit is missing or invalid.

The runtime-family protocol and publisher pipeline are specified in Reusable native runtime architecture. The protected CPython 3.14 wheels reuse the exact publisher-produced runtime bytes for their platform; application builds never compile, patch, or re-sign those bytes.

The existing protection contract also includes:

  • Every artifact receives a random 128-bit filename, for example myapp/7b82a40e9aa14e60db7094f2de3fbc33.pye.
  • PYE2 has no plaintext JSON header; an artifact contains only its magic value, nonce, and ciphertext/authentication tag.
  • Module names, the entry point, and package flags are stored in an encrypted module index.
  • Manifest format 3 is signed with Ed25519 and contains the SHA-256 digest of every artifact.
  • The runtime, launcher, support trees, and resources copied by default are signed, key-bound, and verified again during install(). Explicit --unsigned-data resources are isolated from that trusted inventory for host-managed mutable data.
  • The loader exposes only a small trampoline to runpy, rather than returning the application's real code object. Module keys are derived on demand and are not retained by the finder.
  • Application modules explicitly selected with --native-module are removed from the encrypted index, emitted as signed ABI-tagged .pyd files, and recorded in signed .pyencode-native.json coverage.
  • A dedicated finder rejects preloaded/shadowed native modules and rechecks the exact extension hash immediately before CPython loads it.

The goal is to make static analysis substantially more expensive than it is for .pyc files. PyEncode does not claim to make reverse engineering impossible.

Supported Python versions

The protected PyPI package for PyEncode 0.7.1 is published as two native wheels for standard CPython 3.14 only: Windows x64 and macOS Apple Silicon. pip selects the matching wheel automatically; the Windows and macOS binaries are not combined into one file. Linux, Intel macOS, free-threaded CPython, PyPy, and other Python versions cannot install these protected 0.7.1 wheels.

The source checkout and portable generated runtime still target standard CPython 3.10 or later. CI monitors Python 3.10 through 3.15, but only CPython 3.14 is a release target until the corresponding native runtime families have been built and validated for more ABIs.

The portable runtime source is Python-only. The default cryptography backend uses that package's native extension; --runtime-crypto pure-python removes the runtime dependency and is intended for restricted embedded hosts such as Fusion on macOS. The bytecode runtime avoids shipping those runtime sources and does not load a Mach-O/PE extension, but .pyc is reversible and does not protect the embedded key as strongly as the reusable native runtime. However, a built artifact is tied to the exact CPython major/minor version used to build it:

  • an artifact built with CPython 3.11 runs on CPython 3.11;
  • that artifact does not run on CPython 3.10, 3.12, or 3.14;
  • to support several minor versions, build a separate output with each interpreter.

If a bundle includes .pyd, .so, .dll, or other native dependencies through --support, the whole bundle is also constrained by the operating system, architecture, and ABI of those files. Current testing targets the standard GIL build of CPython; free-threaded and debug ABIs require separate builds and testing.

Each initial reusable native family supports one exact standard-GIL CPython 3.14 target: Windows x86-64 uses _runtime.cp314-win_amd64.pyd, while macOS Apple Silicon uses _runtime.cpython-314-darwin.so and the cp314-macosx_arm64 asset target. A native build fails closed if the signed catalog, runtime hash/size, runtime_id, ABI, or permit does not match. It does not compile a replacement or silently select a portable backend. Application modules selected with --native-module remain a separate Windows-only advanced feature and still have their own toolchain needs.

On Linux, or on a Mac for which no matching publisher-signed runtime asset is available, select --runtime-backend python or bytecode explicitly. Building and signing the common macOS runtime is a publisher release operation described below; ordinary PyEncode customers do not run it. CPython 3.10 and later remain supported by the portable runtime, subject to the exact major/minor artifact binding described above.

Installation

Install the published package from PyPI with standard CPython 3.14. On Windows:

py -3.14 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pyencode-protector

On an Apple-Silicon Mac, use an arm64 CPython 3.14 environment:

python3.14 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pyencode-protector

The wheel contains a small public bootstrap plus a level-2 encrypted payload. It uses the prebuilt native runtime for the selected platform; installing the package does not compile or sign a new .pyd/.so. The protected payload also contains a byte-identical, vendor-verified runtime pack and the matching protected offline permit capability used when the installed CLI protects another native application.

The CLI command and import namespace are both named pyencode.

For source development, use an editable installation in a virtual environment.

Windows PowerShell:

py -3.11 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e .

Linux:

python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .

The builder depends on cryptography>=42. Portable generated applications use it at runtime by default. Pass --runtime-crypto pure-python to emit a dependency-free portable runtime that verifies Ed25519 signatures and opens the existing AES-256-GCM PYE2 format using signed Python code. This compatibility backend is slower and is not constant-time; it is intended for small protected bundles in embedded hosts, not as a general-purpose cryptography library. The native runtime has no generated-application dependency on cryptography or _pure_crypto.py.

Users of the protected platform wheels do not need a compiler, signing tool, separate runtime download, or permit service to use the reusable native runtime. The compiler toolchain and platform code-signing credentials belong to the publisher release pipeline. They are not used to customize the common runtime on a customer machine.

Usage

Built-in command-line help is available without opening the documentation:

pyencode --help
pyencode build --help
python -m pyencode --help

Protect a single file:

pyencode build hello.py -o dist/hello
python dist/hello/run.py

Protect a package containing __main__.py:

pyencode build src/myapp -o dist/myapp
python dist/myapp/run.py

Specify the entry module explicitly:

pyencode build src -o dist/app --entry myapp.__main__

Emit a CPython-bound sourceless runtime without native binaries:

pyencode build src -o dist/app --entry myapp.main \
  --obf-code 1 \
  --runtime-backend bytecode \
  --runtime-crypto pure-python

This mode emits __init__.pyc, _mp_main.pyc, _runtime.pyc, _pure_crypto.pyc, and _build.pyc directly inside pyencode_runtime. Build it with the same CPython major/minor used by the target host.

Reusable native runtime (self-contained offline flow)

On a supported CPython 3.14 platform, the protected PyPI wheel contains the matching publisher-produced runtime pack and its vendor trust material. It also contains a target-specific offline permit capability inside the level-2 encrypted PyEncode payload. A normal native build therefore needs only the native backend and its mandatory protection level:

pyencode build src -o dist/app --entry myapp.main `
  --obf-code 2 `
  --runtime-backend native

The same command works in a CPython 3.14 arm64 environment on macOS using normal shell line continuations. It does not contact a permit service and does not need --runtime-asset, --runtime-vendor-key, or --permit-signing-key.

The bundled pack contains one exact runtime binary -- _runtime.cp314-win_amd64.pyd for Windows x64 or _runtime.cpython-314-darwin.so for macOS arm64 -- plus canonical, vendor-signed runtime-asset.json. The metadata binds the immutable binary's filename, SHA-256, size, ABI, runtime_id, RSA-3072 wrapping public key, and Ed25519 permit issuer. The CLI verifies that catalog and the exact runtime bytes, creates a fresh 32-byte application master key and build_id, encrypts the source locally, and signs the exact build permit through the protected offline capability. The application source and plaintext master key never leave the machine.

_seal.json is mandatory and authenticated independently by the permit issuer. It is deliberately absent from the manifest's integrity.files: putting the permit inside the manifest commitment that the permit itself signs would create a hash cycle. At startup the native runtime verifies the canonical permit and issuer signature, verifies the manifest signature, recomputes the full application commitment, and compares it with the permit before unwrapping or using the application master key. The seal remains a required member of the immutable distribution inventory even though it is outside integrity.files.

The runtime .pyd or .so is copied byte-for-byte so its catalog digest and any platform signature remain valid. Users do not build, patch, rename, or re-sign it. If the exact asset, vendor key, runtime_id, target, or signed permit cannot be verified, the build fails; native mode never compiles a local substitute or downgrades to Python or bytecode. --runtime-backend native also requires --obf-code 2 and rejects levels 0 and 1.

The explicit runtime and permit options remain available as advanced overrides. --runtime-asset and --runtime-vendor-key must be supplied together; an external pack also needs its matching --permit-signing-key or a complete --permit-url/--license-key service configuration. A complete service configuration overrides the bundled offline signer for the normal bundled pack. The ordinary source checkout contains no publisher material, so native builds from an editable/source installation also require these explicit inputs.

For a native runtime, --runtime-crypto is compatibility metadata and does not select a Python crypto implementation. The native runtime uses compiled platform cryptography and has no generated-application dependency on cryptography or _pure_crypto.py.

Publisher provisioning, signing, rotation, and the optional online permit-service contract are documented in Reusable native runtime architecture.

Publisher-only Windows common-runtime build

This command is for the PyEncode runtime publisher, not for a customer protecting an application. Run it on Windows x64 with standard-GIL CPython 3.14 and a pinned native-toolchain descriptor. Authenticode remains the default, fail-closed policy; it additionally requires Windows SDK SignTool, a trusted Code Signing certificate, and the certificate provider's RFC 3161 timestamp URL.

List usable certificates and copy the intended certificate's thumbprint exactly:

Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert |
  Format-Table Subject, Thumbprint, NotAfter, HasPrivateKey

Then run the production-only wrapper, replacing the certificate and timestamp placeholders with values supplied for that signing identity:

& .\tools\build_common_runtime_windows.ps1 `
  -Python "C:\Python314\python.exe" `
  -Toolchain ".\build\native-toolchain.json" `
  -SigningThumbprint "<40-HEX-CERTIFICATE-THUMBPRINT>" `
  -TimestampUrl "<RFC3161-TIMESTAMP-URL>"

Use -CertificateStore local-machine when the certificate is in LocalMachine\My, and -SignTool if Windows SDK SignTool is not discoverable. The wrapper installs cryptography into an isolated temporary directory and removes it afterward; pass -NoBootstrap only when that dependency is already available to the selected CPython 3.14 interpreter. The SHA-1 thumbprint selects the certificate; the binary and RFC 3161 timestamp digests are still SHA-256.

To deliberately publish the common Windows runtime without Authenticode, use the explicit unsigned policy and omit every signing option:

& .\tools\build_common_runtime_windows.ps1 `
  -Python "C:\Python314\python.exe" `
  -Toolchain ".\build\native-toolchain.json" `
  -Unsigned

-Unsigned is not a fallback: the build fails if the freshly compiled PE does not report the exact Windows Authenticode status NotSigned. It never discovers or invokes SignTool and never requests a timestamp. Supplying -SigningThumbprint, -TimestampUrl, -CertificateStore, or -SignTool together with -Unsigned is an error. Here “unsigned” applies only to Windows Authenticode; the immutable runtime catalog is still signed by the separate vendor Ed25519 key.

Both policies compile and inspect the PE, verify the applicable Authenticode state, then re-inspect and import the final .pyd. Only after those checks does the tool calculate the final SHA-256 and create the vendor-signed runtime-asset.json and release ZIP. Never sign, patch, strip, or re-sign the .pyd afterward because any byte change invalidates the catalog.

The public return defaults to build/pyencode-common-cp314-win-amd64-v1-return/; its sibling *-publisher-private/ directory is a publisher key vault and must never be copied to Git, PyPI, an application, or a customer. Use -KeyPasswordFile to encrypt its PKCS#8 files in addition to the restricted NTFS ACL. Customers reuse the immutable catalog-bound runtime and do not need SignTool, a certificate, Windows SDK, Cython, or Zig.

Publisher-only macOS common-runtime build

This command is for the PyEncode runtime publisher, not for a customer protecting an application. Run it from the prepared repository on an Apple Silicon Mac with Xcode Command Line Tools and a usable Developer ID Application identity in the login keychain:

bash tools/build_common_runtime_macos.sh

The script first tries /opt/homebrew/bin/python3. If that default is missing or is not standard-GIL CPython 3.14 for arm64, it downloads a pinned CPython 3.14.7 Apple-Silicon python-build-standalone archive, verifies its fixed SHA-256, and uses it only inside the isolated temporary build directory. The downloaded interpreter, archive, virtual environment, Cython, and cryptography are removed on success or failure. Pass --no-python-bootstrap to prohibit that download. An explicitly selected --python or PYENCODE_BUILD_PYTHON remains fail-closed and is never silently replaced.

The script detects an unambiguous Developer ID Application identity, builds the exact _runtime.cpython-314-darwin.so, signs it with a secure timestamp and hardened-runtime options, and fails closed if the Mach-O target, export, dependencies, signature, or post-signing digest is wrong. It never accepts an ad-hoc identity for a release runtime.

Use explicit values when the Mac has several signing identities, Python is elsewhere, the family is being rotated, or notarization credentials are configured:

bash tools/build_common_runtime_macos.sh \
  --python /opt/homebrew/bin/python3 \
  --identity "Developer ID Application: Publisher Name (TEAMID)" \
  --runtime-id pyencode-common-cp314-macosx-arm64-v1 \
  --output build/pyencode-common-cp314-macosx-arm64-v1-return \
  --notary-profile pyencode-notary

--notary-profile is optional. When supplied, the script submits the exact signed runtime archive with xcrun notarytool, waits for Accepted, and records the submission result and log. Omitting it produces a Developer-ID-signed but not notarized candidate; that distinction must remain visible in release notes and must not be represented as a notarized production asset.

The script generates fresh RSA unwrap, Ed25519 permit-issuer, and Ed25519 vendor keys for the new family. It writes the complete public release handoff to the selected --output directory and writes the private keys to a different, create-only publisher vault. The defaults are:

build/pyencode-common-cp314-macosx-arm64-v1-return/
|-- runtime-release/
|   |-- _runtime.cpython-314-darwin.so
|   `-- runtime-asset.json
|-- pyencode-common-cp314-macosx-arm64-v1-runtime-release.zip
|-- SHA256SUMS.txt
|-- public/
|   |-- wrap-public.der
|   |-- issuer-public.raw
|   `-- vendor-public.raw
|-- evidence/
|   |-- runtime-handoff.json
|   |-- codesign.txt
|   |-- lipo.txt
|   |-- nm.txt
|   |-- otool.txt
|   |-- otool-headers.txt
|   |-- otool-load-commands.txt
|   |-- import-smoke.txt
|   |-- notary-submission.json          # only with --notary-profile
|   |-- notary-log.json                 # only with --notary-profile
|   `-- notary-requirement.txt          # only with --notary-profile
|-- pyencode-common-cp314-macosx-arm64-v1-notary-upload.zip  # optional
`-- COPY-BACK.txt

build/pyencode-common-cp314-macosx-arm64-v1-publisher-private/
|-- unwrap-private.pem
|-- issuer-private.pem
|-- vendor-private.pem
|-- issuer-public.raw
|-- vendor-public.raw
|-- wrap-public.der
|-- family-private.json
`-- DO-NOT-SHIP.txt

runtime-release/ and its *-runtime-release.zip are already the complete, canonical, vendor-signed runtime asset; Windows must verify and consume them, not regenerate runtime-asset.json. public/ contains the exact public material for cross-checking the catalog, but a trusted pinned copy of the vendor key remains the trust root; proximity to runtime-asset.json is not authentication. evidence/ records the signed binary and build checks. When notarization is not requested, runtime-handoff.json records not-submitted and no notary-only files or upload ZIP are produced.

Copy only the *-return directory through the ordinary handoff and verify SHA256SUMS.txt after transfer. Never combine it with the sibling *-publisher-private vault. The vault is created with restrictive permissions, but its PKCS#8 keys are password-encrypted only when --key-password-file is supplied. Back it up or provision its issuer/vendor credentials through a separate publisher-controlled secret channel; never place it in Git, a protected app, a customer ZIP, or the public return directory.

Do not run stapler against the bare .so or its ZIP. Apple can notarize an archive and issue an online ticket for the nested standalone binary, but does not support stapling a ticket directly to either item. A distributable outer app, bundle, disk image, or installer should be notarized and stapled by its publisher when that format supports stapling.

A valid Developer ID signature and an accepted notarization are necessary release checks, not a promise that every embedding host will load the extension. Fusion's hardened-runtime library-validation policy is independent and can reject a third-party Team ID. Validate the exact runtime in the released Fusion version from a quarantined, freshly extracted add-in, then close and reopen Fusion before publishing the family.

At level 2, a load such as obj.member, a store such as obj.member = value, and deletion or augmented assignment of that attribute retain normal descriptor and evaluation-order semantics, while member is absent from the transformed executable operation. Method calls and longer chains are transformed the same way. Public function, class, method, and callback names are not renamed.

Application AOT is independent of the reusable runtime. --native-module may still compile selected application implementation modules through a pinned local toolchain; it neither changes the common runtime nor exposes its unwrap key or a plaintext permit-signing key file. This advanced feature is not required for ordinary native-runtime builds.

Sign and key-bind additional dependencies or support trees:

pyencode build src -o dist/app \
  --entry myapp.main \
  --support build/vendor _vendor \
  --support public/config.json config.json

--support SOURCE DEST may be repeated. If SOURCE is a directory, its tree is copied below DEST; if it is a file, DEST is the output file path. Symlinks, reparse points, paths that escape the root, and case-insensitive path collisions are rejected.

The exact destination _vendor is treated as a bootstrap dependency path for hosts that do not already have cryptography installed, such as Fusion. Only the exact _vendor entry remains on sys.path while the verifier loads; every other bundle subdirectory is isolated. Because the vendored cryptography library must run before Ed25519 can verify the bundle itself, _vendor is a trusted bootstrap component by design in the default cryptography runtime. Vendor only wheels obtained from trusted sources.

For an embedded host that disallows the native extension in cryptography, do not vendor it. Build instead with the wire-compatible dependency-free backend:

pyencode build src -o dist/app --entry myapp.main \
  --runtime-crypto pure-python

This writes an empty generated requirements.txt. Both cryptographic implementations consume the same signed manifest and PYE2 AES-256-GCM artifacts, so the choice does not change the authentication algorithms or container format. Pure-Python AES and big-integer Ed25519 operations are not constant-time and decrypt more slowly.

For Fusion on macOS, use this portable combination when no compatible, publisher-signed cp314-macosx_arm64 runtime asset has been released:

pyencode build src -o dist/app --entry myapp.main \
  --obf-code 1 \
  --runtime-backend python \
  --runtime-crypto pure-python

Main options:

  • -o, --output: output directory; it must not exist or must be completely empty.
  • -e, --entry: dotted name of the entry module or package.
  • --exclude GLOB: exclude paths matching a glob; may be repeated.
  • --no-resources: do not copy non-Python files from the source tree.
  • --keep-docstrings: preserve docstrings.
  • --optimize {0,1,2}: CPython optimization level.
  • --expires YYYY-MM-DD: refuse to run after the specified UTC date.
  • --launcher PATH: use a custom .py launcher.
  • --support SOURCE DEST: copy, sign, and key-bind a support file or tree.
  • --rename-locals: rename metadata for local variables that are not parameters.
  • --obf-code {0,1,2}: choose module-only decryption (0, the default), add authenticated per-function capsules and dispatch stubs (1), or additionally encrypt eligible statically spelled attribute names and dispatch their load/store/delete operations through the runtime protector (2). The reusable native backend always requires level 2.
  • --allow-extra-data: allow the host to create additional unsigned data files and directories; signed files and recognized Python/native artifact types remain protected. The application must not execute or trust the added data.
  • --unsigned-data PATTERN: copy matching discovered source resources only after the signed/key-bound inventory is sealed; repeat for additional globs. This requires --allow-extra-data.
  • --runtime-crypto {cryptography,pure-python}: select the portable Python runtime's cryptographic implementation. The default is cryptography; the native runtime always uses its compiled platform boundary.
  • --runtime-backend {python,bytecode,native}: emit the portable Python runtime (the default), five sourceless CPython-minor-bound .pyc runtime files, or one publisher-built, vendor-catalog-bound reusable runtime. Native mode has no portable or locally compiled fallback.
  • --runtime-asset DIR: override the protected wheel's bundled runtime with a directory containing the exact runtime binary and canonical signed runtime-asset.json. Use it together with --runtime-vendor-key.
  • --runtime-vendor-key FILE: override the bundled Ed25519 vendor public key used to verify runtime asset metadata. Use it together with --runtime-asset.
  • --runtime-id ID: additionally require an exact immutable runtime-family version for either the bundled or overridden asset.
  • --permit-signing-key FILE: publisher/development override that signs permits with a matching offline Ed25519 issuer key instead of the protected wheel's bundled capability.
  • --permit-url HTTPS_URL: override offline permit issuance by requesting authorization from a license service; it defaults to PYENCODE_PERMIT_URL.
  • --license-key KEY: supply a PyEncode builder license. Prefer the PYENCODE_LICENSE_KEY environment variable so it is not recorded in shell history. A service URL and license credential must be configured together.
  • --permit-timeout SECONDS: bound the permit-service request time.
  • --native-toolchain FILE: use a canonical, hash-pinned Windows toolchain only for the independent --native-module application-AOT feature. It is not used to build or alter the reusable runtime.
  • --native-module MODULE: compile this exact dotted implementation module as a native extension; repeat for additional modules. Package __init__, entry, __main__, top-level, duplicate, missing, globbed, and non-ASCII names fail.

--rename-locals is opt-in because code that uses a dynamic alias of locals(), reads frame.f_locals, or depends on debugger/framework introspection may require the original local names. Arguments, closures, line tables, and exception tables are always preserved.

--obf-code, --optimize, --rename-locals, and --keep-docstrings govern encrypted .pye modules. Native-selected application modules use the fixed directives recorded in .pyencode-native.json (binding=False, no embedded signatures/code comments, no tracing/profile hooks, and docstrings disabled) so coverage is auditable.

Custom launchers and embedded hosts

A custom launcher is copied byte-for-byte, signed, and included in key derivation. It must set:

import sys
sys.dont_write_bytecode = True

before any non-bootstrap import. The builder enforces this minimum requirement. A secure production launcher must also verify that pyencode_runtime contains no __pycache__, .pyc, or unexpected files before importing the package. The allowed set must name the selected backend exactly: _runtime.py for the Python backend, or the target-specific reusable extension plus _seal.json for a common native family. The default launcher and the Weldments pipeline include this complete preflight check.

A host such as Fusion can load an entry point without placing its plaintext module name in the stub:

import sys
sys.dont_write_bytecode = True

# Perform the pyencode_runtime preflight here.
from pyencode_runtime import load_entry

implementation = load_entry()

install() intentionally returns None. load_entry() returns the entry module to an embedding host, while run() executes the entry point with __main__ semantics.

Output layout and runtime behavior

The native layout below shows both target alternatives for reference; one build contains exactly one of the two reusable runtime extensions, never both.

dist/app/
├── .pyencode-manifest.json
├── .pyencode-native.json               # only with application AOT
├── .pyencode-runtime-native.json       # reusable runtime_id/asset report
├── myapp/
│   ├── 14a0...f91c.pye
│   ├── 7b82...bc33.pye
│   ├── core.cp314-win_amd64.pyd        # only if selected by --native-module
│   └── assets/config.json
├── pyencode_runtime/
│   ├── __init__.py
│   ├── _build.py
│   ├── _mp_main.py
│   ├── _pure_crypto.py                 # Python backend only
│   ├── _runtime.py                     # Python backend only
│   ├── _runtime.cp314-win_amd64.pyd    # Windows reusable family
│   ├── _runtime.cpython-314-darwin.so  # macOS arm64 target alternative
│   └── _seal.json                      # signed permit + wrapped app key
├── requirements.txt
└── run.py

The native runtime report records the exact runtime_id, target, and signed asset hash. The copied runtime bytes are identical to the publisher asset. This report is distinct from .pyencode-native.json, which records only application modules compiled through --native-module. Both reports are signed when present. For a common native runtime, _seal.json is the one deliberate exception to the manifest integrity.files set: its own canonical Ed25519 permit signature and the permit's application_commitment_sha256 provide the non-circular binding.

Resources are not encrypted, but their build-time bytes and paths are signed and key-bound. By default, the entire output tree is immutable: the runtime rejects any file or directory that is added, removed, or modified.

For hosts that create metadata or caches beside an add-in, --allow-extra-data permits additional data files and directories without invalidating the bundle. This mode still rejects modifications or removal of signed files, unexpected .py, .pyc, native libraries, or .pye files, and all symlinks or reparse points. Do not use this extra data as trusted input for licensing or security decisions. The Weldments pipeline enables this compatibility mode so Fusion can create .vscode, logs, or machine-specific caches.

Files that the application itself updates, such as user settings, host metadata, or profile libraries, must not be part of the signed source inventory. Use repeatable --unsigned-data patterns to copy their initial values safely without a separate post-build step:

pyencode build src -o dist/app --entry myapp.main \
  --allow-extra-data \
  --unsigned-data '*.png' \
  --unsigned-data 'myapp/data/settings.json'

Patterns are matched case-sensitively against the POSIX path relative to the output root and, for convenience, against the basename. Thus *.png selects PNG resources at any depth; **/*.png is also accepted and includes root-level matches. Every pattern must match at least one resource discovered in the main source tree. --unsigned-data does not apply to --support inputs, and --no-resources leaves nothing eligible for selection.

Selected files are copied into the atomic temporary output only after the signed manifest and key-bound inventory have been sealed. Their paths and bytes are absent from integrity.files, are not inputs to key derivation, and may be modified or removed by the host. The signed manifest records only the existing allow_extra_data=true compatibility policy; it deliberately does not present the patterns or selected files as trusted inputs.

The builder rejects a pattern if it selects Python source/bytecode, .pye, native libraries, executables or executable scripts, symlinks/reparse points, non-regular entries, the launcher, pyencode_runtime, either generated manifest/report, or requirements.txt. Added unsigned files are still scanned at runtime: recognized code/native artifacts and every link/reparse point remain forbidden. Never execute or use unsigned data for licensing, integrity, or other security decisions.

The __file__ value of a protected module is the real path to its randomly named artifact. Consequently, Path(__file__).parent, pkgutil.get_data(), and importlib.resources, including nested directory resources, continue to work. Code that depends on the basename or stem of __file__ sees the random token.

Integration tests cover imports by full name, relative imports, circular imports, namespace packages, Unicode module names, reloads, runpy.run_module(), and multiprocessing spawn for applications launched through the default launcher. pkgutil.iter_modules() and walk_packages() cannot automatically discover protected child names because those names intentionally reside in the encrypted index; importing a known full name still works.

For an embedding host that calls load_entry(), spawning works if the child process also runs the host bootstrap. A host that places its bootstrap only inside if __name__ == '__main__' and then directly spawns a protected target requires a dedicated launcher. Automatic spawn bootstrapping currently focuses on applications executed through run().

Versions before 1.0 use one fixed runtime namespace per process. Do not load two independent PyEncode distributions into the same interpreter; run them in separate processes. Randomized or multi-bundle runtime namespaces are a compatibility item to resolve before declaring a stable 1.0 API.

Security limitations

PyEncode makes static analysis harder, but it cannot guarantee secrecy on a machine fully controlled by an attacker:

  • Portable execution is offline, so key material must remain recoverable from the bundle. An experienced analyst can inspect the Python runtime and reproduce key derivation. The reusable native runtime instead embeds one common-family private unwrap key in compiled code. Each build has a unique application master key and stores only its RSA-OAEP-wrapped form in the signed permit. Binary analysis, native memory inspection, or a debugger can still recover these values.
  • The common runtime is intentionally shared. If an attacker extracts its private unwrap key, every protected application using that runtime_id is inside the same compromise domain. Unique application master keys prevent direct key reuse but do not contain this family-wide blast radius. Recovery requires publishing a new immutable family and rebuilding applications; rotation cannot retroactively protect offline outputs made with the old family.
  • The self-contained protected wheel intentionally carries an offline permit capability. Its issuer material is not shipped as a plaintext PEM/resource: it is placed inside a function capsule in a level-2 encrypted .pye module and is usable only after the protected PyEncode payload and native runtime load. This raises static extraction cost, but it is not a hard licensing boundary. A user who fully controls the process may invoke, hook, or recover that capability and issue builds without an online authorization decision. Use a wheel without the embedded capability plus a permit service when server-enforced builder licensing or revocation is required.
  • Code objects must exist in memory while they execute. Tracing, debuggers, memory inspection, code-object watchers, or native hooks may still observe code and runtime state. The native codec avoids the ordinary marshal, code.__new__, and function.__new__ audit paths, but this does not make it debugger- or PyCode_AddWatcher-proof.
  • At --obf-code 1, the original code for protected top-level functions is not present in the module code object at rest, and normal function.__code__ inspection sees a generic stub. The real CodeType is nevertheless materialized for execution. A suspended generator/coroutine retains an executing frame, and an attacker who controls the process can inspect or intercept it. This option raises extraction cost; it is not a confidentiality boundary.
  • At --obf-code 2, ordinary source-level obj.attribute operations carry an encrypted attribute-name capsule instead of placing that spelling in the executable operation. The runtime must nevertheless decrypt and materialize the real name to use Python's object model. The portable runtime caches validated names as Python strings. The Windows native runtime instead caches UTF-8 names in an opaque C hash table, exposes only a PyCapsule to Python GC, and zeroes each allocation when released; a transient str still exists for every actual attribute operation. An attacker controlling the interpreter can inspect execution or invoke a captured capsule against a proxy object which reports the requested name. Explicit strings passed to getattr/setattr, annotation metadata, dotted value/class patterns, __slots__, string literals, and other reflective data are intentionally not rewritten. The conservative local/frame-introspection guard attributes used by --rename-locals also remain visible so that enabling both options does not defeat the guard. Level 2 replaces CPython's specialized LOAD_ATTR/LOAD_METHOD path with runtime dispatch, so attribute-heavy code may be materially slower and should be benchmarked against the real workload. On CPython 3.13 or newer it can also change introspection-only type.__static_attributes__ metadata because the compiler sees a protected subscription rather than STORE_ATTR; code relying on that metadata should remain at level 1.
  • The integrity interlock prevents straightforward file modification, artifact substitution, code injection, and re-signing. It does not turn a pure-Python runtime into a native trust anchor. A native runtime is a stronger obfuscation boundary, not a hardware-backed trust anchor.
  • Module and entry-point names are hidden at rest, but appear during import in sys.modules, tracebacks, and runtime state. Package and resource directories may still reveal part of the application structure.
  • --expires relies on the system clock and is not a replacement for a licensing system.
  • Native-selected modules no longer expose a marshal-loadable application code object through the PyEncode runtime, but Cython binaries still reveal useful names, strings, metadata, and machine code to a skilled reverse engineer.
  • Native runtime and application extensions remain patchable in a process fully controlled by an attacker. The signed finder raises the cost of preload, path-shadow, and file-tamper attacks; it is not an unpatchable hardware trust anchor or a guarantee of equivalence to any PyArmor protection level or configuration. Matching the documented attribute- chain semantics of its level 2 does not reproduce PyArmor's proprietary runtime or separate RFT/BCC/private/restrict features. The products use different runtimes and should be compared with repeatable attacks against the exact builds, not by matching option names.
  • The native runtime is loaded before it can verify the application inventory. Its vendor-signed metadata and exact hash detect accidental corruption and inconsistent replacement, but are not hardware trust anchors. The publisher must finish the declared Windows signing policy (Authenticode or verified NotSigned) or sign the macOS .so before computing and signing runtime-asset.json; protected wheels and generated applications then copy those bytes unchanged. A notarized macOS runtime also remains subject to the embedding host's independent library-validation policy.
  • The native CodeType record uses CPython's C pickle implementation only after the signed artifact hash and AES-GCM tag have been verified. Its schema checks are for trusted build output; it is not a general-purpose decoder for untrusted pickle data.
  • Native files are re-hashed immediately before CPython loads them, but the hash and operating-system loader open are not one atomic operation. An attacker that already controls the process or filesystem may still exploit that narrow replacement window.

Never ship a plaintext permit-signing key file, publisher code-signing credential, vendor metadata-signing key, or plaintext application master key. The protected wheel's deliberate offline permit capability is an obfuscated self-hosting convenience, not server-enforced licensing. When the optional service override is used, it sends only a canonical permit request and license credential; it does not send source or the plaintext master key. A reusable native runtime and --obf-code 2 primarily add reverse-engineering cost. End-user application licensing, online revocation, or device binding remains a separate design.

Testing

python -m unittest discover -v

CI runs on Windows and Ubuntu with CPython 3.10 through 3.15, plus macOS CPython 3.14 compatibility jobs. Portable and bytecode tests cover PYE2, the encrypted module index, KDF vectors, tampering and re-signing, resource/support integrity, opaque filenames, imports, packages, namespaces, Unicode, runpy, multiprocessing spawn, ordinary launchers without -B, expiration policy, and atomic output creation. Level-1 tests cover build-time function capsules, ordinary functions and methods, lambdas, closures, generators, coroutines, call behavior, and the idle dispatch-stub invariant. Level-2 tests additionally cover encrypted attribute loads, method calls, chains, stores, deletion, augmented and unpacking assignment, descriptor behavior, private-name mangling, tamper rejection, and the annotation, pattern-matching, and local-reflection exclusions.

The reusable-runtime suite covers strict canonical asset metadata, vendor signatures, exact target filename/hash/size checks, symlink and path rejection, byte-identical copying, per-application RSA key wrapping, signed build permits, target and runtime_id binding, the mandatory full-manifest application commitment, the independently authenticated seal outside integrity.files, HTTPS permit-client behavior, and tamper rejection. Publisher-only Windows jobs may also compile and inspect test binaries; they do not constitute or publish a production Authenticode-signed runtime asset. Application-AOT tests remain independent of this runtime-family contract.

Production macOS runtime jobs must build, Developer ID sign, optionally submit for notarization, inspect, import, and run the exact immutable family before its metadata is published. A production family advertised as notarized must have an Accepted notary result. That publisher pipeline is not a customer build requirement, and Fusion compatibility still requires a quarantined host-level load test.

Releasing

The production release process using GitHub OIDC is documented in RELEASING.md. A tag alone never publishes source. The workflow uploads only the two audited protected wheels attached to a published GitHub Release whose tag exactly matches the project version, for example v0.7.1.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pyencode_protector-0.7.1-cp314-cp314-win_amd64.whl (1.6 MB view details)

Uploaded CPython 3.14Windows x86-64

pyencode_protector-0.7.1-cp314-cp314-macosx_11_0_arm64.whl (1.5 MB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

File details

Details for the file pyencode_protector-0.7.1-cp314-cp314-win_amd64.whl.

File metadata

File hashes

Hashes for pyencode_protector-0.7.1-cp314-cp314-win_amd64.whl
Algorithm Hash digest
SHA256 42c993b7376fbe730af631dac72170683243105e94a82d8cec41132e1a0cbed4
MD5 95afc2681b1b32ff415b3c5def626b50
BLAKE2b-256 c00f5ef21ad4fbb3a0a23feb17f640b9c4ba386bc2490d32eaba1b1f0f429477

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyencode_protector-0.7.1-cp314-cp314-win_amd64.whl:

Publisher: release.yml on VanThanBK/pyencode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyencode_protector-0.7.1-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pyencode_protector-0.7.1-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c64b61063550a367929164759b520c67f5f67cffc3b36ad06bbb4db731c316a2
MD5 5472af5ffdd0c3dd69021453fa321efa
BLAKE2b-256 c4a9ff0ce35e0e6651273c17ba12b63cf399ccf0bd65e27f861889e0b661ebaa

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyencode_protector-0.7.1-cp314-cp314-macosx_11_0_arm64.whl:

Publisher: release.yml on VanThanBK/pyencode

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.7.1 This release

2 files

0.7.0

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.3.1

2 files

0.3.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page