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, pyproject.toml, tests/, VS Code debugger settings, a CI workflow, and .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 prints a QR code that opens the project on a phone, watches app/, syncs every save to each connected dev client with Fast Refresh, relays their logs, and serves the browser preview, DevTools, and the debugger proxy. It reads single-key commands (i, a, w, j, r, m, d, q) when stdin is a terminal. Every client needs your dev token from ~/.pythonnative/dev-token, which the printed URLs carry. Flags: --port (default 8765), --host (default 0.0.0.0), --debug-port (default 5678; 0 disables it), --no-interactive, --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, --debug-port, --no-interactive, --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 passed to the app; include the ?token= from pn start), --port (where pn start listens). When no Android device is running it boots an emulator, and it opens the app with a connect link (pn-<app id>://connect?url=...).
  • pn go android|ios: install PythonNative Go, the prebuilt dev client, and open the running dev server in it. The artifact matching this pythonnative version comes from the cache, the GitHub release (verified against its SHA-256), or a one-time local build. Flags: --device, --build (build locally), --port.
  • 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 lint [paths...]: check the rules of hooks without running the code. With no paths it lints app/, or the current directory if there's no app/. Each finding prints as path:line:col: CODE message. Flag: --json to print a JSON array of findings to stdout for scripting; the summary line goes to stderr instead. Exits 1 when there are findings and 2 when a path doesn't exist. See the Linting guide.
  • pn codegen: generate native contract files and print each generated file's path. Flags: --output to choose the output directory (default generated), and --module to import a module that declares extension schemas before generation. Repeat --module for each module; each must be importable by the Python interpreter running pn. For example, to write the contracts to an explicit temporary directory:

    pn codegen --output /tmp/pythonnative-contracts
    

    This command generates contract files only. Native compilation and app rebuilding happen as part of the build workflow. See Native contracts for the extension workflow and Generated source and vendored dependencies for repository contributor guidance.

  • 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 prints a QR code that opens the project on a phone, renders the app in a browser tab, serves DevTools and the debugger proxy, syncs every save to every connected dev client with Fast Refresh, and reads single-key commands (i, a, r, j, ...); device logs stream back into the same terminal.
  • pn preview: pn start plus opening the browser preview.
  • pn go android|ios: install PythonNative Go, the prebuilt dev client, and open the running pn start in it.
  • 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 the project's own debug build and connect it 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.
  • pn lint [paths...]: check the rules of hooks without running the code, as path:line:col: CODE message lines or as JSON with --json.

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.

go_command

Install PythonNative Go and open the running pn start in it.

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.

lint_command

Check the rules of hooks and exit 1 when anything is found.

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_DEBUG_PORT

Port of pn start's debugger proxy unless --debug-port says otherwise.

DEFAULT_DEV_PORT module-attribute

DEFAULT_DEV_PORT = 8765

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

DEFAULT_DEBUG_PORT module-attribute

DEFAULT_DEBUG_PORT = 5678

Port of pn start's debugger proxy unless --debug-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 also writes pyproject.toml (pinning this pythonnative and declaring pytest and mypy), tests/test_app.py, VS Code's debugger configuration and extension recommendations, and a GitHub Actions workflow that runs pn lint, mypy, and pytest.

It won't scaffold into a target directory that already holds files, and it won't overwrite any of the scaffolded paths; 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).

required

go_command

go_command(args: Namespace) -> None

Install PythonNative Go and open the running pn start in it.

Go is the prebuilt dev client: it runs any project that adds no native code, without building the project. The artifact for this framework version comes from the cache, the GitHub release, or (once) a local build; see pythonnative.project.go.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace (platform, device, build, port).

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

lint_command

lint_command(args: Namespace) -> None

Check the rules of hooks and exit 1 when anything is found.

Lints each path in args.paths (files, or directories searched recursively for *.py). With no paths it lints app/ when that directory exists and the current directory otherwise. The checks themselves live in lint_paths.

Without --json, each finding prints as path:line:col: CODE message followed by a summary line. With --json, stdout carries a JSON array and nothing else, one object per finding with path, line, col, code, and message keys, and the summary goes to stderr. Either way the exit status is 1 when there are findings, 0 when there aren't, and 2 when a path doesn't exist.

Parameters:

Name Type Description Default
args Namespace

Parsed namespace with paths and optional json.

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