Skip to content

CLI (pn)

Reference for the pn console script. The implementation lives in pythonnative.cli.pn; this page renders its docstrings directly so the documented behavior never drifts from the code.

Subcommands

  • pn init [name]: scaffold a new project (creates app/, pythonnative.toml, .gitignore). With a name it creates ./<name>/ and scaffolds into it; the name must match ^[a-z][a-z0-9_-]*$. Without one it uses the current directory, whatever it's called. Flag: --force to overwrite existing files or scaffold into a non-empty directory. See Configuration.
  • pn doctor [android|ios]: diagnose the local toolchain and validate pythonnative.toml, including that a python3.X matching [app].python_version is available for package resolution. Exits non-zero when something will block a build. Flag: --json to print a JSON array of check results to stdout for scripting; the verdict line goes to stderr instead, and the exit status is unchanged.
  • pn deps [android|ios]: resolve [requirements].packages for every device target (iOS device, iOS Simulator, and each Android ABI) without installing anything, and report the wheel each package would use, flagging binary wheels, their index, and downgrades. Flags: --json for a machine-readable report, --python to pick the interpreter that runs pip. Exits non-zero when any target can't be satisfied. See PyPI packages.
  • pn start [entry]: run the dev server. It watches app/, syncs every save to each connected debug build with Fast Refresh, relays their logs, and serves the browser preview page. Flags: --port (default 8765), --host (default 0.0.0.0), --open to also open the browser preview. See the Development workflow.
  • pn preview [entry]: pn start plus opening the browser preview in your default browser. Flags: --port, --host, --no-open. See the Browser preview guide.
  • pn devices [android|ios]: list connected devices, emulators, and simulators with the identifiers --device accepts. Flag: --json to print a JSON array to stdout for scripting; the "no devices" hints go to stderr instead, and an empty list prints [] and exits 0.
  • pn run android|ios: build, install, and launch a debug build that connects to the running dev server. The native toolchain runs only when a native input changed (config, template, pythonnative itself, native plugins); otherwise the previous artifact is reinstalled. Flags: --device (target a specific device by identifier or name), --prepare-only, --no-logs, --rebuild (force the toolchain), --dev-server URL (override the server URL baked into the app), --port (where pn start listens), --dev-client (build a shell app with a connect screen that loads any project from a dev server).
  • pn logs android|ios: stream logs from the running app without rebuilding. Flag: --device (target a specific device by identifier or name, same as pn run). Physical iOS devices aren't supported for log streaming; use Console.app or Xcode > Devices and Simulators.
  • pn build android|ios: build distributable artifacts (release by default). Flags: --debug for the debug variant, --upload to send an iOS release build to App Store Connect. See Building for release.
  • pn app-id android|ios: print the resolved application id (Android) or bundle id (iOS), handy for scripts and CI. Flag: --json to print a {"platform": "...", "app_id": "..."} object to stdout for scripting; the config error goes to stderr instead, and a missing config still exits 1.
  • pn clean: remove the local build/ directory.
  • pn --version (-V): print the installed PythonNative version.

pn CLI: scaffold, diagnose, start, run, and build PythonNative apps.

The console script pn (declared in pyproject.toml) dispatches to:

  • pn init [name]: scaffold a new project (pythonnative.toml + app/) into ./name/, or into the current directory when no name is given.
  • pn doctor [platform]: diagnose the local toolchain and config, as a report or as JSON with --json.
  • pn deps [platform]: resolve [requirements].packages for every device target and report which wheels would be used (or why a package can't be installed), without building anything.
  • pn start: run the dev server. It renders the app in a browser tab and syncs every save to every connected debug build (simulators, emulators, physical devices) with Fast Refresh; device logs stream back into the same terminal.
  • pn preview: pn start plus opening the browser preview.
  • pn devices [platform]: list connected devices, emulators, and simulators, as a table or as JSON with --json.
  • pn run android|ios [--device D]: stage + build + install + launch a debug build that connects to the dev server. The native project is only rebuilt when something outside app/ changed.
  • pn logs android|ios [--device D]: stream logs from the running app without rebuilding.
  • pn build android|ios: produce standalone artifacts (signed APK/AAB, or an iOS archive/IPA, optionally uploaded to App Store Connect).
  • pn app-id android|ios: print the resolved application/bundle id (handy for scripts and CI), as plain text or as JSON with --json.
  • pn clean: remove the local build/ directory.

The heavy lifting lives in the pythonnative.project and pythonnative.devserver packages; this module is a thin, side-effect-y shell that wires arguments to them and handles the device-facing steps (simulator boot, launch, log streaming) that can't be unit tested.

Functions:

Name Description
init_project

Scaffold a new PythonNative project.

doctor_command

Run toolchain/config diagnostics and exit non-zero on errors.

app_id_command

Print the resolved application id (Android) or bundle id (iOS).

deps_command

Report how [requirements].packages resolve for each device target.

start_command

Run the dev server (and the browser preview) for the current project.

preview_command

pn preview: pn start that also opens the browser preview.

devices_command

List connected devices, emulators, and simulators.

run_project

Stage, build, install, and launch a debug build that talks to pn start.

build_project

Build standalone, distributable artifacts for platform.

logs_command

Stream logs from the running app without rebuilding.

clean_project

Remove the local build/ directory.

codegen_command

Generate native contracts after importing extension schema modules.

main

Entry point for the pn console script.

Attributes:

Name Type Description
DEFAULT_DEV_PORT

Port pn start listens on unless --port says otherwise.

DEFAULT_DEV_PORT module-attribute

DEFAULT_DEV_PORT = 8765

Port pn start listens on unless --port says otherwise.

init_project

init_project(args: Namespace) -> None

Scaffold a new PythonNative project.

Given a name, this creates ./<name>/ and scaffolds into it. Without one, it scaffolds into the current directory and names the project after it. Either way it writes app/main.py, pythonnative.toml, and .gitignore.

A name you pass has to match ^[a-z][a-z0-9_-]*$: lowercase letters, digits, -, and _, starting with a letter. Anything else is refused with a legal suggestion. That keeps the directory name and the name field in the generated config identical, in the same spirit as flutter create and cargo new. The name taken from the current directory when you pass none is used as-is, so an existing directory with any name still works.

The name also has to be a single directory name, so the project always lands inside the current directory. Anything that reads as a path, such as a/b, .., or /tmp/app, is refused, and so is a name that resolves somewhere else, such as a symlink to another directory.

It won't scaffold into a target directory that already holds files, and it won't overwrite any of the three paths above; pass --force to override both. An existing but empty target directory is fine. A plain file at ./<name> is always refused, since --force can't turn it into a directory, and --force lifts neither of the rules above.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace with name (optional) and force.

required

doctor_command

doctor_command(args: Namespace) -> None

Run toolchain/config diagnostics and exit non-zero on errors.

With --json, stdout carries a JSON array and nothing else, one object per check (see CheckResult.to_dict), and the verdict line goes to stderr. The exit status stays keyed to the worst check level either way.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace with optional platform and json.

required

app_id_command

app_id_command(args: Namespace) -> None

Print the resolved application id (Android) or bundle id (iOS).

With --json, stdout carries one object, {"platform": "...", "app_id": "..."}, and nothing else; the config error goes to stderr instead so stdout stays parseable. Keys may be added to that object later, but none will be removed or renamed, so a caller reading app_id keeps working.

A missing or invalid config still exits 1, with or without the flag. That differs from pn devices --json, which exits 0 on an empty result, because "no devices" is a valid answer to a query whereas an unreadable config means there is no id to report at all.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace with platform and optional json.

required

deps_command

deps_command(args: Namespace) -> None

Report how [requirements].packages resolve for each device target.

Runs pip in its cross-platform dry-run mode once per target (iOS device, iOS Simulator, and one per Android ABI) and prints the wheel each package would use, flagging binary wheels and their source index. Exits non-zero when any target can't be satisfied, so it doubles as a CI gate. --json emits the same data as a machine-readable document.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace with optional platform, json, and python (the interpreter to run pip with).

required

start_command

start_command(
    args: Namespace, *, open_browser: bool = False
) -> None

Run the dev server (and the browser preview) for the current project.

Re-execs under PN_PLATFORM=web so every module binds to the browser backend, then hands off to pythonnative.preview.serve.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace (entry, host, port, open, no_open).

required
open_browser bool

Open the preview page once the server is up (pn preview sets this; --open does too).

False

preview_command

preview_command(args: Namespace) -> None

pn preview: pn start that also opens the browser preview.

devices_command

devices_command(args: Namespace) -> None

List connected devices, emulators, and simulators.

Prints an aligned table and exits 1 when nothing is connected.

With --json, stdout carries a JSON array and nothing else, one object per device (see Device.to_dict), so it stays parseable. The "no devices" hints go to stderr instead, an empty result prints [], and the exit status is 0 either way, since "no devices" is a valid answer for a script rather than a failure.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace with optional platform and json.

required

run_project

run_project(args: Namespace) -> None

Stage, build, install, and launch a debug build that talks to pn start.

The native toolchain only runs when a native input changed (see pythonnative.project.fingerprint) or when no dev server is up to deliver the latest sources; otherwise the previous artifact is reinstalled, which turns the edit/relaunch loop from minutes into seconds.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace (platform, device, prepare_only, no_logs, rebuild, dev_server, port, dev_client).

required

build_project

build_project(args: Namespace) -> None

Build standalone, distributable artifacts for platform.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace (platform, debug, upload).

required

logs_command

logs_command(args: Namespace) -> None

Stream logs from the running app without rebuilding.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace (platform, device).

required

clean_project

clean_project(args: Namespace) -> None

Remove the local build/ directory.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace (unused).

required

codegen_command

codegen_command(args: Namespace) -> None

Generate native contracts after importing extension schema modules.

main

main() -> None

Entry point for the pn console script.

Next steps