Building for release¶
pn run is for iterating on a device or simulator. When you're ready to
ship, pn build produces standalone, distributable artifacts:
Pass --debug to build the debug variant instead (a debug APK on
Android; a Simulator .app on iOS):
pn build reads identity, permissions, assets, and signing from
pythonnative.toml. Run pn doctor
first to confirm the toolchain is ready.
Check first: pn doctor¶
pn doctor validates pythonnative.toml and checks the platform
toolchain: Android needs JDK 17 through 23, the configured SDK (36 by
default), NDK 28.2.13676358, and adb. iOS needs macOS, Xcode 26 or
later, and simctl. The doctor also reports signing, lockfile, and
privacy-manifest configuration. It
exits non-zero on anything that will block a build, so you can gate CI
on it.
Frozen dependencies¶
Release builds with Python dependencies require a current pn.lock covering
all build targets. Resolve and review the lock before building:
pn deps --lock prints the full resolution report first. A lock has to
cover every target, so if any target fails to resolve, it says which ones
failed, doesn't write pn.lock, and exits with status 1. Fix the failing
requirement (or the target's wheel availability) and run it again.
Commit the lock with your application. Changing requirements or replacing a
local wheel requires refreshing it. Release builds reject missing or stale
locks. A development build (pn run, or pn build --debug) with a stale
lock prints a warning and resolves the current requirements without the lock,
so you can keep iterating before you refresh it. Native extension manifests
pin SwiftPM and Maven dependencies to exact versions.
What a release bundle leaves out¶
Every build bundles a copy of the pythonnative package. Debug builds
are dev clients, so their copy is complete. Release builds leave out the
modules that exist only for development:
- the
pnCLI and build tooling (pythonnative/cli,pythonnative/project); - the dev server and browser preview (
pythonnative/devserver,pythonnative/preview.py,pythonnative/bridge/web.py); - the dev client and Fast Refresh (
pythonnative/devclient.py,pythonnative/hot_reload.py,pythonnative/refresh.py); - the test helpers (
pythonnative/testing,pythonnative/bridge/fake.py); - the SDK code generators (
pythonnative/sdk/codegen.py,builtins.py,module_codegen.py, and their templates).
The runtime never imports them, and the test suite checks that it stays
that way, so a release app behaves exactly like a debug build that isn't
connected to pn start. If your own code imports one of these modules,
it works in debug builds but fails with ModuleNotFoundError in a
release build; keep such imports in development-only code paths.
iOS release builds also omit the NSLocalNetworkUsageDescription that
debug builds add so they can reach pn start over Wi-Fi. If your app
uses the local network itself, declare the key with your own wording in
[ios].extra_info_plist.
Android¶
pn build android runs assembleRelease and bundleRelease, producing
both an APK and a Play-ready AAB. Artifacts are reported at the end of
the build and live under the staged project:
build/android/android_template/app/build/outputs/apk/release/app-release.apk
build/android/android_template/app/build/outputs/bundle/release/app-release.aab
Toolchain and packaged binaries¶
The template uses compile/target SDK 36, Gradle 8.13, Android Gradle Plugin 8.11.1, Kotlin 2.2.21, and NDK 28.2.13676358. Release builds reject a target SDK below 36. These are PythonNative's supported build defaults, independent of store submission deadlines.
After Gradle builds, PythonNative inspects the APK and AAB. Every packaged 64-bit ELF library must have 16 KB aligned LOAD segments, including Python extensions inside Chaquopy assets. Uncompressed APK libraries must also be ZIP aligned, and the AAB must request compatible generated APK alignment. An error names the offending dependency. Rebuild or replace that dependency; upgrading your app's NDK can't repair a prebuilt wheel. See Android's 16 KB page-size guidance.
Signing¶
Without signing configured, Gradle emits an unsigned release APK
(app-release-unsigned.apk), fine for inspection, not for the store.
To produce signed artifacts, create a keystore once:
keytool -genkeypair -v -keystore release.keystore \
-alias myapp -keyalg RSA -keysize 2048 -validity 10000
…then point pythonnative.toml at it:
[android.signing]
keystore = "release.keystore"
key_alias = "myapp"
# store_password_env / key_password_env default to
# PN_ANDROID_KEYSTORE_PASSWORD / PN_ANDROID_KEY_PASSWORD
Passwords are never stored in the config, only the names of the environment variables that hold them. Provide the secrets at build time:
When signing is configured, pn injects a Gradle signingConfig into
the release build so the resulting APK/AAB are signed and upload-ready.
Keep keystores out of git
Commit neither the keystore nor the passwords. Store the keystore as a CI secret/file and inject the passwords via environment variables.
iOS¶
pn build ios archives the app with xcodebuild archive and exports a
signed .ipa with xcodebuild -exportArchive. The Python framework,
standard library, and app code are installed and signed during the
Xcode build itself, so the archive needs no post-processing. Outputs:
To send the build straight to App Store Connect instead of exporting a
local .ipa, set export_method = "app-store" and pass --upload:
The upload uses the App Store Connect credentials Xcode has stored; in
CI, provide an ASC API key through Xcode's standard mechanisms or
upload the exported .ipa with xcrun altool/Transporter instead.
Privacy manifests¶
The template includes an app PrivacyInfo.xcprivacy. Supply your app's own
manifest with a path relative to the project:
PythonNativeKit ships its own resource-bundle manifest for preferences and cache-file timestamps. Archive validation checks that the app and runtime manifests are present and structurally valid before export or upload. It also parses manifests supplied by bundled dependencies. This check doesn't infer your application's data collection, tracking, or required-reason API usage. Declare those from your actual behavior using Apple's privacy-manifest documentation.
Bytecode everywhere¶
An app bundle is read-only, so the device can't cache what it compiles.
pn therefore compiles ahead of time, in every build:
- Release builds byte-compile your
app/sources, every bundled package, and the embedded standard library to sourceless.pycfiles and drop the.pyfiles. That shrinks the bundle, speeds up launch, and avoids shipping plain-text source. - Debug builds (
pn run,pn build --debug) keep the.pyfiles beside__pycache__bytecode, so tracebacks show source lines and Fast Refresh can read sources, while launch still skips compilation. Sources the dev client syncs are cached on the device after their first import.
Bytecode records bundle-relative file names (app/main.py), never paths
on your build machine. Because bytecode is version-specific, compiling
needs an interpreter matching app.python_version: the one running pn,
or python3.13 (for example) on your PATH. Release builds fail without
one; debug builds warn and let the device compile at launch. Android uses
Chaquopy's own compilation and has no such requirement.
Signing¶
Set your Apple Developer Team ID and an export method:
[ios]
development_team = "ABCDE12345"
[ios.signing]
export_method = "app-store" # development | ad-hoc | app-store | enterprise
provisioning_profile = "My App Distribution"
development_team drives signing during archive; export_method and
the optional provisioning_profile are written into the
exportOptions.plist that xcodebuild -exportArchive consumes. If
export fails, the error points you back at [ios.signing].
Embedded Python runtime¶
iOS has no system Python, so PythonNative embeds CPython from the
Python-Apple-support
project. The first iOS build on a machine downloads the pinned,
checksum-verified runtime for your app.python_version into a cache
every project shares (~/Library/Caches/pythonnative/ios/, or
$PN_CACHE_DIR/ios/) and prepares it once: the standard library loses
CPython's test suite, GUI toolkits, installers, and test-only extension
modules, and the rest is compiled to bytecode. pn clean doesn't touch
this cache. The Xcode build links Python.xcframework, installs the
prepared standard library, and bundles your app/ sources, the
pythonnative package, and the [requirements].packages resolved for
the SDK being built (device or Simulator), binary wheels included.
Pinned, verified runtimes exist for Python 3.13 and 3.14; set
python_version in [app] accordingly. Unpinned versions are
rejected rather than fetched unverified. See
PyPI packages for how requirements are resolved
per target.
App icon and splash¶
Provide a single high-resolution source image and PythonNative renders every per-platform, per-density variant at build time:
- iOS: a universal
AppIcon.appiconset(Xcode resizes the rest) and aSplashimage set referenced by the generated launch screen. - Android:
mipmap-*launcher icons at every density plus a round variant, and a centered icon for the Android 12+ splash screen.
Image processing needs Pillow, an optional dependency:
If Pillow isn't installed the build still succeeds; it just keeps the
template's default assets. pn doctor reports whether Pillow is
available.
Versioning¶
Two fields in [app] drive the store-visible version and the internal
build number:
[app]
version = "1.2.0" # CFBundleShortVersionString / versionName
build = 7 # CFBundleVersion / versionCode (bump every upload)
Both stores reject a new upload that reuses an existing build number, so
increment build for each submission.
Continuous integration¶
A typical release job:
pip install 'pythonnative[build]'
pn doctor android # fail fast on a misconfigured runner
export PN_ANDROID_KEYSTORE_PASSWORD=$KEYSTORE_PW
export PN_ANDROID_KEY_PASSWORD=$KEY_PW
pn build android
pn app-id android / pn app-id ios print the resolved id, which is
handy for downstream steps (uploaders, smoke tests) that need it without
re-parsing the config.
Prepare without building¶
To hand off to Android Studio or Xcode instead of building from the CLI, stage and configure the native project without compiling:
This writes a fully configured project (identity, permissions, icons,
relocated Android package) under build/, which you can open and build
with the native IDE, useful for debugging signing or build issues with
the platform's own tooling.