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 (createsapp/,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:--forceto overwrite existing files or scaffold into a non-empty directory. See Configuration.pn doctor [android|ios]: diagnose the local toolchain and validatepythonnative.toml, including that apython3.Xmatching[app].python_versionis available for package resolution. Exits non-zero when something will block a build. Flag:--jsonto 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].packagesfor 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:--jsonfor a machine-readable report,--pythonto 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, watchesapp/, 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(default0.0.0.0),--debug-port(default 5678; 0 disables it),--no-interactive,--opento also open the browser preview. See the Development workflow.pn preview [entry]:pn startplus 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--deviceaccepts. Flag:--jsonto 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,pythonnativeitself, 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=frompn start),--port(wherepn startlistens). 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 thispythonnativeversion 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 aspn 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:--debugfor the debug variant,--uploadto 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:--jsonto 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 localbuild/directory.pn lint [paths...]: check the rules of hooks without running the code. With no paths it lintsapp/, or the current directory if there's noapp/. Each finding prints aspath:line:col: CODE message. Flag:--jsonto 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:--outputto choose the output directory (defaultgenerated), and--moduleto import a module that declares extension schemas before generation. Repeat--modulefor each module; each must be importable by the Python interpreter runningpn. For example, to write the contracts to an explicit temporary directory: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].packagesfor 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 startplus opening the browser preview.pn go android|ios: install PythonNative Go, the prebuilt dev client, and open the runningpn startin 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 outsideapp/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 localbuild/directory.pn lint [paths...]: check the rules of hooks without running the code, aspath:line:col: CODE messagelines 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 |
start_command |
Run the dev server (and the browser preview) for the current project. |
preview_command |
|
devices_command |
List connected devices, emulators, and simulators. |
run_project |
Stage, build, install, and launch a debug build that talks to |
go_command |
Install PythonNative Go and open the running |
build_project |
Build standalone, distributable artifacts for |
logs_command |
Stream logs from the running app without rebuilding. |
clean_project |
Remove the local |
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 |
Attributes:
| Name | Type | Description |
|---|---|---|
DEFAULT_DEV_PORT |
Port |
|
DEFAULT_DEBUG_PORT |
Port of |
DEFAULT_DEV_PORT
module-attribute
¶
Port pn start listens on unless --port says otherwise.
DEFAULT_DEBUG_PORT
module-attribute
¶
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 |
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 |
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 |
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 |
required |
start_command
¶
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 ( |
required |
open_browser
|
bool
|
Open the preview page once the server is up
( |
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 |
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 ( |
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 ( |
required |
build_project
¶
build_project(args: Namespace) -> None
Build standalone, distributable artifacts for platform.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
args
|
Namespace
|
Parsed namespace ( |
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 ( |
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 |
required |
Next steps¶
- See the Getting started walkthrough.
- Read the Development workflow for how
pn startandpn runwork together.