Skip to content

Dev server and dev client

The development loop is a server process (pn start) and any number of clients: the browser preview and debug builds on simulators, emulators, and devices. See the Development workflow guide for how they fit together; this page documents the modules.

pythonnative.devserver

The PythonNative dev server: one process that serves every dev client.

pn start (and pn preview, which is pn start plus a browser tab) runs a DevServer. It watches the project's app/ directory, keeps a content-addressed manifest of the sources, and speaks a small JSON protocol over WebSocket to two kinds of peers:

  • Dev clients: debug builds of the app running on a simulator, emulator, or physical device. They sync sources into an on-device overlay, Fast Refresh when files change, and stream their logs and errors back to the terminal. See pythonnative.devclient.
  • The browser preview: a page served by this server that renders the app through the bridge protocol, exactly as the Swift and Kotlin runtimes do. See pythonnative.bridge.web.

Everything here is standard library only (asyncio streams plus a hand-rolled RFC 6455 implementation in pythonnative.devserver.ws) so the same code also runs inside the embedded interpreter on device.

Modules:

Name Description
server

The dev server: HTTP for static assets, WebSocket for live peers.

watcher

Source snapshots and change detection for the dev server.

ws

A small RFC 6455 (WebSocket) implementation on the standard library.

Classes:

Name Description
DevServer

Serve sources, assets, and the live protocol for one project.

ServerInfo

What the server is serving and where.

FileWatcher

Poll a source tree and report changes on a background thread.

SourceSnapshot

The state of a source tree at one instant.

Functions:

Name Description
lan_addresses

Best-effort list of this machine's non-loopback IPv4 addresses.

snapshot_sources

Hash every synced file under root/<subdir> for each of subdirs.

Attributes:

Name Type Description
DEFAULT_PORT

Default port for pn start / pn preview.

DEFAULT_PORT module-attribute

DEFAULT_PORT = 8765

Default port for pn start / pn preview.

DevServer

DevServer(
    project_root: str,
    entry_module: str = "app.main",
    *,
    host: str = "0.0.0.0",
    port: int = DEFAULT_PORT,
    project_name: str = "",
    static_dir: Optional[str] = None,
    log: Optional[Logger] = None,
    watch: bool = True
)

Serve sources, assets, and the live protocol for one project.

Parameters:

Name Type Description Default
project_root str

Directory containing app/ and pythonnative.toml.

required
entry_module str

The app's entry module ("app.main").

'app.main'
host str

Bind address. 0.0.0.0 so devices on the LAN can reach it; pass 127.0.0.1 to stay local.

'0.0.0.0'
port int

TCP port; 0 picks a free one.

DEFAULT_PORT
project_name str

Shown in the preview page and /status.

''
static_dir Optional[str]

Where the preview page's assets live.

None
log Optional[Logger]

Where to print client logs and connection events.

None
watch bool

Whether to run the file watcher.

True

Methods:

Name Description
start

Bind the socket on a background thread and return the address.

stop

Close every connection and stop the thread.

set_preview_channel

Install the handler for the browser preview's bridge traffic.

add_change_listener

Be told (on the watcher thread) when sources change; returns an unsubscribe.

manifest

The /manifest document.

status

The /status document.

Attributes:

Name Type Description
snapshot SourceSnapshot

The current source snapshot.

clients List[DevClient]

Connected dev clients (a copy).

snapshot property

snapshot: SourceSnapshot

The current source snapshot.

clients property

clients: List[DevClient]

Connected dev clients (a copy).

start

start() -> ServerInfo

Bind the socket on a background thread and return the address.

Raises:

Type Description
OSError

When the port is taken (the CLI prints a hint).

stop

stop() -> None

Close every connection and stop the thread.

set_preview_channel

set_preview_channel(
    channel: Optional[PreviewChannel],
) -> None

Install the handler for the browser preview's bridge traffic.

add_change_listener

add_change_listener(
    listener: Callable[
        [SourceChange, SourceSnapshot], None
    ],
) -> Callable[[], None]

Be told (on the watcher thread) when sources change; returns an unsubscribe.

manifest

manifest() -> Dict[str, Any]

The /manifest document.

status

status() -> Dict[str, Any]

The /status document.

ServerInfo dataclass

ServerInfo(
    host: str,
    port: int,
    project_root: str,
    entry_module: str,
    project_name: str = "",
)

What the server is serving and where.

Methods:

Name Description
url

The HTTP base URL, substituting host for the bind address.

ws_url

The dev-client WebSocket URL.

Attributes:

Name Type Description
display_host str

A host suitable for a URL (0.0.0.0 becomes localhost).

display_host property

display_host: str

A host suitable for a URL (0.0.0.0 becomes localhost).

url

url(host: Optional[str] = None) -> str

The HTTP base URL, substituting host for the bind address.

ws_url

ws_url(host: Optional[str] = None) -> str

The dev-client WebSocket URL.

FileWatcher

FileWatcher(
    root: str,
    on_change: Callable[
        [SourceChange, SourceSnapshot], None
    ],
    *,
    subdirs: Sequence[str] = ("app",),
    interval: float = 0.25,
    settle: float = 0.08
)

Poll a source tree and report changes on a background thread.

Parameters:

Name Type Description Default
root str

Project root.

required
on_change Callable[[SourceChange, SourceSnapshot], None]

Called with a SourceChange and the new snapshot after every poll that found changes.

required
subdirs Sequence[str]

Directories under root to watch.

('app',)
interval float

Seconds between polls.

0.25
settle float

Seconds a change must be stable before it is reported, so an editor's write-then-rename or a multi-file save lands as one reload instead of several.

0.08

Methods:

Name Description
start

Start polling on a daemon thread (idempotent).

stop

Stop polling and join the thread.

poll

Scan once and return the change since the last scan (or None).

Attributes:

Name Type Description
snapshot SourceSnapshot

The most recent snapshot.

snapshot property

snapshot: SourceSnapshot

The most recent snapshot.

start

start() -> None

Start polling on a daemon thread (idempotent).

stop

stop() -> None

Stop polling and join the thread.

poll

Scan once and return the change since the last scan (or None).

SourceSnapshot dataclass

SourceSnapshot(
    root: str,
    files: Dict[str, str] = dict(),
    mtimes: Dict[str, Tuple[int, int]] = dict(),
    version: str = "",
)

The state of a source tree at one instant.

Attributes:

Name Type Description
root str

Absolute directory the relative paths are resolved against (the project root; paths start with app/).

files Dict[str, str]

relative_path -> sha256 for every synced file.

mtimes Dict[str, Tuple[int, int]]

relative_path -> (mtime_ns, size) used to skip re-hashing unchanged files between polls.

version str

A digest of the whole tree, stable across processes for identical contents.

Methods:

Name Description
read

Return the current bytes of rel_path (None if it is gone).

diff

Describe how to get from self to other.

read

read(rel_path: str) -> Optional[bytes]

Return the current bytes of rel_path (None if it is gone).

diff

diff(other: 'SourceSnapshot') -> 'SourceChange'

Describe how to get from self to other.

lan_addresses

lan_addresses() -> List[str]

Best-effort list of this machine's non-loopback IPv4 addresses.

Used to print a URL a physical device on the same Wi-Fi can reach.

snapshot_sources

snapshot_sources(
    root: str,
    subdirs: Sequence[str] = ("app",),
    *,
    previous: Optional[SourceSnapshot] = None
) -> SourceSnapshot

Hash every synced file under root/<subdir> for each of subdirs.

Parameters:

Name Type Description Default
root str

Project root.

required
subdirs Sequence[str]

Directories (relative to root) to include.

('app',)
previous Optional[SourceSnapshot]

An earlier snapshot; files whose (mtime, size) are unchanged reuse their digest instead of being re-read.

None

Server

The dev server: HTTP for static assets, WebSocket for live peers.

One DevServer runs on its own thread with a private asyncio loop, so it works both inside pn start (whose main thread runs the browser preview's app) and in tests. It exposes:

  • GET /: the browser preview page.
  • GET /static/<name>: preview assets (JS, CSS).
  • GET /manifest: {"version", "entry", "files": {path: sha256}}.
  • GET /file/<path>: raw bytes of one synced source file.
  • GET /status: server, project, and connected-peer information.
  • WS /ws?role=client: the dev-client protocol (see below).
  • WS /ws?role=preview: the browser preview's bridge channel; the server only relays text frames between the page and the PreviewChannel handler installed by the preview.

Dev-client protocol (JSON objects, one per text frame):

  • client -> server hello: {"type": "hello", "platform", "device", "app", "files": {path: sha256}} describing what the client already holds in its overlay.
  • server -> client sync: {"type": "sync", "version", "entry", "files": [{"path", "sha256", "content", "encoding"}], "removed": [...]} bringing the client up to date. The same shape is sent as "update" whenever the watcher sees a change.
  • client -> server log (level, text), error (phase, text), reloaded (version, mode, modules): streamed to the terminal.

Classes:

Name Description
ServerInfo

What the server is serving and where.

PreviewPeer

A connected browser preview page; send is safe from any thread.

PreviewChannel

What the preview installs to receive the page's bridge traffic.

DevServer

Serve sources, assets, and the live protocol for one project.

Functions:

Name Description
lan_addresses

Best-effort list of this machine's non-loopback IPv4 addresses.

Attributes:

Name Type Description
DEFAULT_PORT

Default port for pn start / pn preview.

DEFAULT_PORT module-attribute

DEFAULT_PORT = 8765

Default port for pn start / pn preview.

ServerInfo dataclass

ServerInfo(
    host: str,
    port: int,
    project_root: str,
    entry_module: str,
    project_name: str = "",
)

What the server is serving and where.

Methods:

Name Description
url

The HTTP base URL, substituting host for the bind address.

ws_url

The dev-client WebSocket URL.

Attributes:

Name Type Description
display_host str

A host suitable for a URL (0.0.0.0 becomes localhost).

display_host property

display_host: str

A host suitable for a URL (0.0.0.0 becomes localhost).

url

url(host: Optional[str] = None) -> str

The HTTP base URL, substituting host for the bind address.

ws_url

ws_url(host: Optional[str] = None) -> str

The dev-client WebSocket URL.

PreviewPeer

PreviewPeer(server: 'DevServer', writer: StreamWriter)

A connected browser preview page; send is safe from any thread.

Methods:

Name Description
send

Queue one text frame to the page (dropped once the peer is closed).

close

Ask the server to close this page's socket.

send

send(text: str) -> None

Queue one text frame to the page (dropped once the peer is closed).

close

close() -> None

Ask the server to close this page's socket.

PreviewChannel

Bases: Protocol

What the preview installs to receive the page's bridge traffic.

Every method is called on the server thread; implementations hop to their own thread as needed.

Methods:

Name Description
on_preview_connected

A page connected (info carries its query parameters).

on_preview_message

A text frame arrived from the page.

on_preview_disconnected

The page went away.

on_preview_connected

on_preview_connected(
    peer: PreviewPeer, info: Dict[str, Any]
) -> None

A page connected (info carries its query parameters).

on_preview_message

on_preview_message(peer: PreviewPeer, text: str) -> None

A text frame arrived from the page.

on_preview_disconnected

on_preview_disconnected(peer: PreviewPeer) -> None

The page went away.

DevClient dataclass

DevClient(
    id: int,
    writer: StreamWriter,
    platform: str = "unknown",
    device: str = "",
    app: str = "",
    connected_at: float = time(),
    files: Dict[str, str] = dict(),
)

One connected on-device dev client.

Methods:

Name Description
label

A short human label for log lines.

label

label() -> str

A short human label for log lines.

DevServer

DevServer(
    project_root: str,
    entry_module: str = "app.main",
    *,
    host: str = "0.0.0.0",
    port: int = DEFAULT_PORT,
    project_name: str = "",
    static_dir: Optional[str] = None,
    log: Optional[Logger] = None,
    watch: bool = True
)

Serve sources, assets, and the live protocol for one project.

Parameters:

Name Type Description Default
project_root str

Directory containing app/ and pythonnative.toml.

required
entry_module str

The app's entry module ("app.main").

'app.main'
host str

Bind address. 0.0.0.0 so devices on the LAN can reach it; pass 127.0.0.1 to stay local.

'0.0.0.0'
port int

TCP port; 0 picks a free one.

DEFAULT_PORT
project_name str

Shown in the preview page and /status.

''
static_dir Optional[str]

Where the preview page's assets live.

None
log Optional[Logger]

Where to print client logs and connection events.

None
watch bool

Whether to run the file watcher.

True

Methods:

Name Description
start

Bind the socket on a background thread and return the address.

stop

Close every connection and stop the thread.

set_preview_channel

Install the handler for the browser preview's bridge traffic.

add_change_listener

Be told (on the watcher thread) when sources change; returns an unsubscribe.

manifest

The /manifest document.

status

The /status document.

Attributes:

Name Type Description
snapshot SourceSnapshot

The current source snapshot.

clients List[DevClient]

Connected dev clients (a copy).

snapshot property

snapshot: SourceSnapshot

The current source snapshot.

clients property

clients: List[DevClient]

Connected dev clients (a copy).

start

start() -> ServerInfo

Bind the socket on a background thread and return the address.

Raises:

Type Description
OSError

When the port is taken (the CLI prints a hint).

stop

stop() -> None

Close every connection and stop the thread.

set_preview_channel

set_preview_channel(
    channel: Optional[PreviewChannel],
) -> None

Install the handler for the browser preview's bridge traffic.

add_change_listener

add_change_listener(
    listener: Callable[
        [SourceChange, SourceSnapshot], None
    ],
) -> Callable[[], None]

Be told (on the watcher thread) when sources change; returns an unsubscribe.

manifest

manifest() -> Dict[str, Any]

The /manifest document.

status

status() -> Dict[str, Any]

The /status document.

lan_addresses

lan_addresses() -> List[str]

Best-effort list of this machine's non-loopback IPv4 addresses.

Used to print a URL a physical device on the same Wi-Fi can reach.

File watcher and source snapshots

Source snapshots and change detection for the dev server.

The dev server treats the project's app/ directory as a content- addressed tree: every dev client gets the same manifest (relative path to SHA-256), so a client can tell exactly which files it is missing after a reconnect, and a change notification can carry the new bytes.

FileWatcher polls the tree with os.stat rather than a native file-system event API so it behaves identically on every host OS without a dependency; the poll interval is short enough that saves feel instant.

Classes:

Name Description
SourceSnapshot

The state of a source tree at one instant.

SourceChange

Paths that changed (added or modified) and paths that disappeared.

FileWatcher

Poll a source tree and report changes on a background thread.

Functions:

Name Description
is_synced_file

Whether a file under app/ takes part in dev sync.

snapshot_sources

Hash every synced file under root/<subdir> for each of subdirs.

MAX_SYNC_FILE_BYTES module-attribute

MAX_SYNC_FILE_BYTES = 8 * 1024 * 1024

Files larger than this are left out of the sync set (they belong in a real build).

SourceSnapshot dataclass

SourceSnapshot(
    root: str,
    files: Dict[str, str] = dict(),
    mtimes: Dict[str, Tuple[int, int]] = dict(),
    version: str = "",
)

The state of a source tree at one instant.

Attributes:

Name Type Description
root str

Absolute directory the relative paths are resolved against (the project root; paths start with app/).

files Dict[str, str]

relative_path -> sha256 for every synced file.

mtimes Dict[str, Tuple[int, int]]

relative_path -> (mtime_ns, size) used to skip re-hashing unchanged files between polls.

version str

A digest of the whole tree, stable across processes for identical contents.

Methods:

Name Description
read

Return the current bytes of rel_path (None if it is gone).

diff

Describe how to get from self to other.

read

read(rel_path: str) -> Optional[bytes]

Return the current bytes of rel_path (None if it is gone).

diff

diff(other: 'SourceSnapshot') -> 'SourceChange'

Describe how to get from self to other.

SourceChange dataclass

SourceChange(
    changed: List[str], removed: List[str], version: str
)

Paths that changed (added or modified) and paths that disappeared.

FileWatcher

FileWatcher(
    root: str,
    on_change: Callable[
        [SourceChange, SourceSnapshot], None
    ],
    *,
    subdirs: Sequence[str] = ("app",),
    interval: float = 0.25,
    settle: float = 0.08
)

Poll a source tree and report changes on a background thread.

Parameters:

Name Type Description Default
root str

Project root.

required
on_change Callable[[SourceChange, SourceSnapshot], None]

Called with a SourceChange and the new snapshot after every poll that found changes.

required
subdirs Sequence[str]

Directories under root to watch.

('app',)
interval float

Seconds between polls.

0.25
settle float

Seconds a change must be stable before it is reported, so an editor's write-then-rename or a multi-file save lands as one reload instead of several.

0.08

Methods:

Name Description
start

Start polling on a daemon thread (idempotent).

stop

Stop polling and join the thread.

poll

Scan once and return the change since the last scan (or None).

Attributes:

Name Type Description
snapshot SourceSnapshot

The most recent snapshot.

snapshot property

snapshot: SourceSnapshot

The most recent snapshot.

start

start() -> None

Start polling on a daemon thread (idempotent).

stop

stop() -> None

Stop polling and join the thread.

poll

Scan once and return the change since the last scan (or None).

is_synced_file

is_synced_file(rel_path: str) -> bool

Whether a file under app/ takes part in dev sync.

Editor swap files, byte-code caches, and VCS metadata are skipped; everything else (Python modules and data files an app reads at runtime) is synced so the on-device overlay mirrors the source tree.

snapshot_sources

snapshot_sources(
    root: str,
    subdirs: Sequence[str] = ("app",),
    *,
    previous: Optional[SourceSnapshot] = None
) -> SourceSnapshot

Hash every synced file under root/<subdir> for each of subdirs.

Parameters:

Name Type Description Default
root str

Project root.

required
subdirs Sequence[str]

Directories (relative to root) to include.

('app',)
previous Optional[SourceSnapshot]

An earlier snapshot; files whose (mtime, size) are unchanged reuse their digest instead of being re-read.

None

modules_for_paths

modules_for_paths(paths: Sequence[str]) -> List[str]

Map synced .py paths (app/screens/home.py) to dotted modules.

WebSocket

A small RFC 6455 (WebSocket) implementation on the standard library.

The dev server and the on-device dev client both need WebSockets, and neither can assume a third-party package: the client runs inside the embedded interpreter on iOS and Android, where every dependency has to be bundled. This module provides just enough of the protocol for a trusted development network:

Extensions (compression) and subprotocols are not negotiated.

Classes:

Name Description
WebSocketError

A framing or protocol violation on the connection.

HandshakeError

The HTTP upgrade did not complete.

FrameDecoder

Incremental frame parser that reassembles fragmented messages.

WebSocketClient

A blocking WebSocket client for background threads.

Functions:

Name Description
accept_key

Return the Sec-WebSocket-Accept value for client_key.

parse_http_headers

Split an HTTP request or response head into (start_line, headers).

server_handshake

Build the 101 Switching Protocols response for an upgrade request.

client_handshake_request

Build a client upgrade request; returns (request_bytes, key).

encode_frame

Encode one frame.

encode_close

Encode a close frame carrying code and reason.

MAX_MESSAGE_BYTES module-attribute

MAX_MESSAGE_BYTES = 64 * 1024 * 1024

Upper bound on one reassembled message; anything larger is a protocol error.

WebSocketError

Bases: Exception

A framing or protocol violation on the connection.

HandshakeError

Bases: WebSocketError

The HTTP upgrade did not complete.

FrameDecoder

FrameDecoder()

Incremental frame parser that reassembles fragmented messages.

Feed raw bytes with feed; it yields complete (opcode, payload) messages. Control frames (ping, pong, close) are yielded as they arrive, even in the middle of a fragmented data message, as the RFC allows.

Methods:

Name Description
feed

Consume data and yield every message it completes.

feed

feed(data: bytes) -> Iterator[Tuple[int, bytes]]

Consume data and yield every message it completes.

WebSocketClient

WebSocketClient(
    url: str, *, timeout: Optional[float] = 30.0
)

A blocking WebSocket client for background threads.

recv blocks until a text message arrives and transparently answers pings; send_text is safe to call from any thread.

Parameters:

Name Type Description Default
url str

ws://host:port/path (wss is not supported; dev traffic stays on the local network).

required
timeout Optional[float]

Connect and read timeout in seconds. Reads that time out raise socket.timeout, letting the owning thread check for shutdown between waits.

30.0

Methods:

Name Description
connect

Open the TCP connection and complete the upgrade handshake.

send_text

Send one text message (masked, as clients must).

recv

Block until a text message arrives.

close

Send a close frame (best effort) and shut the socket.

Attributes:

Name Type Description
connected bool

Whether the socket is open.

connected property

connected: bool

Whether the socket is open.

connect

connect() -> None

Open the TCP connection and complete the upgrade handshake.

send_text

send_text(text: str) -> None

Send one text message (masked, as clients must).

recv

recv() -> Optional[str]

Block until a text message arrives.

Returns None once the peer closes. Raises socket.timeout when the read timeout elapses with no data, so callers can poll a shutdown flag.

close

close(code: int = 1000, reason: str = '') -> None

Send a close frame (best effort) and shut the socket.

accept_key

accept_key(client_key: str) -> str

Return the Sec-WebSocket-Accept value for client_key.

parse_http_headers

parse_http_headers(
    raw: bytes,
) -> Tuple[str, Dict[str, str]]

Split an HTTP request or response head into (start_line, headers).

Header names are lower-cased. raw should be the bytes up to (and optionally including) the blank line that ends the head.

server_handshake

server_handshake(headers: Dict[str, str]) -> bytes

Build the 101 Switching Protocols response for an upgrade request.

Parameters:

Name Type Description Default
headers Dict[str, str]

Lower-cased request headers (see parse_http_headers).

required

Raises:

Type Description
HandshakeError

When the request is not a WebSocket upgrade.

client_handshake_request

client_handshake_request(
    host: str, path: str, key: Optional[str] = None
) -> Tuple[bytes, str]

Build a client upgrade request; returns (request_bytes, key).

encode_frame

encode_frame(
    opcode: int,
    payload: bytes,
    *,
    mask: bool = False,
    fin: bool = True
) -> bytes

Encode one frame.

Clients must send masked frames and servers unmasked ones; the caller picks. Text payloads must already be UTF-8 encoded.

encode_close

encode_close(
    code: int = 1000,
    reason: str = "",
    *,
    mask: bool = False
) -> bytes

Encode a close frame carrying code and reason.

pythonnative.devclient

The on-device dev client: sync sources from pn start and Fast Refresh.

A debug build of a PythonNative app is a dev client. On launch it connects to the dev server (pn start / pn preview / pn run) over WebSocket, reports the sources it already holds, receives whatever is missing, and from then on applies every save as a Fast Refresh. Its print output and errors stream back to the terminal running the server, so the device log viewer is optional.

How the pieces fit:

  • The native template configures a writable overlay directory (pythonnative.hot_reload.configure_dev_environment) ahead of the bundled sources on sys.path and exposes the server URL the CLI baked in as PN_DEV_SERVER. pythonnative.bootstrap.start(dev=True) calls start_if_configured.
  • A build made with pn run <platform> --dev-client has no app of its own: its bundled app/main.py renders ConnectScreen, where the developer types (or picks) a server URL. Once the first sync lands, the real app.main from the overlay shadows the placeholder and the screen remounts into the developer's app. The URL is remembered for the next launch.

All network I/O runs on a daemon thread; file writes happen there too, and only the reload itself hops to the main thread through call_on_main_thread.

Classes:

Name Description
DevClient

Keep this process in sync with a dev server.

Functions:

Name Description
normalize_server_url

Turn whatever the developer typed into a dev-client WebSocket URL.

saved_server_url

The URL remembered by a previous connection (--dev-client builds).

current

The running dev client, if any.

start

Start (or replace) the process-wide dev client for url.

stop

Stop the process-wide dev client.

start_if_configured

Start the dev client when the build points at a server.

Attributes:

Name Type Description
ConnectScreen Any

Root component of --dev-client builds until the first sync arrives.

SERVER_URL_ENV module-attribute

SERVER_URL_ENV = 'PN_DEV_SERVER'

Environment variable carrying the dev server URL baked in by pn run.

DEV_CLIENT_ENV module-attribute

DEV_CLIENT_ENV = 'PN_DEV_CLIENT'

Set to 1 in --dev-client builds (the connect screen remembers URLs).

ConnectScreen module-attribute

ConnectScreen: Any = _LazyConnectScreen()

Root component of --dev-client builds until the first sync arrives.

DevClient

DevClient(
    url: str,
    overlay: str,
    *,
    entry_module: str = "app.main",
    forward_logs: bool = True,
    log: Optional[Logger] = None
)

Keep this process in sync with a dev server.

Parameters:

Name Type Description Default
url str

Server URL (any form accepted by normalize_server_url).

required
overlay str

Writable directory that shadows the bundled sources.

required
entry_module str

The app's entry module, for the hello message.

'app.main'
forward_logs bool

Mirror print output to the server.

True
log Optional[Logger]

Local logger for the client's own status lines.

None

Methods:

Name Description
add_listener

Subscribe to (state, detail) changes (called on the client thread).

start

Connect on a daemon thread (idempotent).

stop

Disconnect and stop the thread.

send

Queue a message to the server (dropped when the outbox overflows offline).

log

Forward one log line to the server.

report_error

Forward an error report (a traceback) to the server.

Attributes:

Name Type Description
state str

"idle", "connecting", "connected", "syncing", or "disconnected".

state property

state: str

"idle", "connecting", "connected", "syncing", or "disconnected".

add_listener

add_listener(
    callback: Callable[[str, str], None],
) -> Callable[[], None]

Subscribe to (state, detail) changes (called on the client thread).

start

start() -> None

Connect on a daemon thread (idempotent).

stop

stop() -> None

Disconnect and stop the thread.

send

send(message: Dict[str, Any]) -> None

Queue a message to the server (dropped when the outbox overflows offline).

log

log(text: str, level: str = 'info') -> None

Forward one log line to the server.

report_error

report_error(phase: str, text: str) -> None

Forward an error report (a traceback) to the server.

normalize_server_url

normalize_server_url(text: str) -> str

Turn whatever the developer typed into a dev-client WebSocket URL.

Accepts 192.168.1.20, 192.168.1.20:8765, http://host:port, ws://host:port, and full URLs with a path; the result always ends in /ws?role=client.

saved_server_url

saved_server_url(
    overlay: Optional[str] = None,
) -> Optional[str]

The URL remembered by a previous connection (--dev-client builds).

current

current() -> Optional[DevClient]

The running dev client, if any.

start

start(
    url: str, overlay: Optional[str] = None, **kwargs: Any
) -> DevClient

Start (or replace) the process-wide dev client for url.

stop

stop() -> None

Stop the process-wide dev client.

start_if_configured

start_if_configured(
    entry_module: Optional[str] = None,
) -> Optional[DevClient]

Start the dev client when the build points at a server.

Called by bootstrap.start(dev=True). The URL comes from PN_DEV_SERVER (baked in by pn run) or, for --dev-client builds, from the URL saved by a previous connection. Returns the client, or None when nothing is configured.

placeholder_main_source

placeholder_main_source() -> str

Source of the app/main.py staged into --dev-client builds.

pythonnative.preview

pn start / pn preview: the dev server plus the browser preview.

serve runs one process that does three jobs:

  1. Dev server (pythonnative.devserver): watches app/, syncs sources to every connected dev client (simulators, emulators, physical devices), and relays their logs to this terminal.
  2. Browser preview: renders the app in a browser tab. The tab is a bridge peer like any device; the reconciler runs in this process on the main thread and commits through WebTransport.
  3. Fast Refresh for the preview: every save reloads the changed modules here and refreshes the mounted screens, exactly as the dev client does on device.

The main thread runs the transport's main loop; that is the browser's stand-in for the UIKit / Android main queue. Everything else (sockets, the file watcher) lives on daemon threads and marshals work onto it.

PN_PLATFORM=web must be set before pythonnative is imported so platform detection binds to the browser backend; the CLI re-execs itself to guarantee that.

Classes:

Name Description
PreviewSession

Everything one pn start invocation owns; see serve.

Functions:

Name Description
serve

Run the dev server (and browser preview) until interrupted.

PreviewSession

PreviewSession(
    project_root: str,
    entry_module: str,
    *,
    host: str = "0.0.0.0",
    port: int = 8765,
    project_name: str = "",
    log: Optional[Logger] = None
)

Everything one pn start invocation owns; see serve.

Methods:

Name Description
start

Install the transport, start the server, and begin watching.

run

Run the main loop until stop (or Ctrl+C).

stop

Tear everything down.

urls

Every URL the server can be reached at (local first).

start

start() -> None

Install the transport, start the server, and begin watching.

run

run() -> None

Run the main loop until stop (or Ctrl+C).

stop

stop() -> None

Tear everything down.

urls

urls() -> List[str]

Every URL the server can be reached at (local first).

serve

serve(
    entry_module: str,
    *,
    project_root: Optional[str] = None,
    host: str = "0.0.0.0",
    port: int = 8765,
    project_name: str = "",
    open_browser: bool = False,
    log: Optional[Logger] = None,
    banner: bool = True,
    ready: Optional[Callable[[PreviewSession], None]] = None
) -> None

Run the dev server (and browser preview) until interrupted.

Parameters:

Name Type Description Default
entry_module str

The app's entry module ("app.main").

required
project_root Optional[str]

Directory containing app/; defaults to the current directory. Added to sys.path.

None
host str

Bind address (0.0.0.0 so devices can connect).

'0.0.0.0'
port int

TCP port (0 picks a free one).

8765
project_name str

Shown in the preview page.

''
open_browser bool

Open the preview page in the default browser.

False
log Optional[Logger]

Where status lines go (stderr by default).

None
banner bool

Print the connection banner.

True
ready Optional[Callable[[PreviewSession], None]]

Called once the server is listening (tests).

None

Raises:

Type Description
RuntimeError

If PN_PLATFORM=web was not set before PythonNative was imported (the CLI sets it for you).

OSError

If the port is taken.

pythonnative.bridge.web

The browser preview's half of the bridge.

pn preview renders an app in a browser tab. Rather than a second rendering backend, the page is treated as the native runtime: it receives the very same JSON transactions the Swift and Kotlin runtimes apply, answers the same synchronous measure / command / animate / call requests, and raises the same callback(kind, tag, name, payload) events. The reconciler therefore runs the on-device BridgeBackend and the on-device NativeScreenHost unchanged; only this transport differs, and it is about moving strings over a WebSocket.

Threads:

  • The main thread owns the framework: it drains WebTransport.run_main_loop, which is the browser's stand-in for the UIKit / Android main queue. Every callback from the page and every asyncio pump runs there.
  • The dev server thread owns the socket. It delivers page messages to the transport, which either settles a waiting request (res) or queues work for the main thread.

Synchronous requests (measure above all) block the main thread on a threading.Event until the page answers; the page is single-threaded but its message handling is asynchronous, so it can answer a measure while it is itself awaiting Python (a row bind, say).

Wire format (JSON arrays):

  • Python -> page: ["apply", ops], ["measure", id, tag, w, h], ["command", id, tag, name, args], ["animate", id, tag, request], ["call", id, module, method, envelope], ["res", id, result], ["dev", {...}].
  • Page -> Python: ["res", id, result], ["cb", kind, tag, name, payload] (fire and forget), ["req", id, kind, tag, name, payload] (Python answers with res), ["gesture", tag, phase, info] (pointer stream for the Python gesture arbiter), ["dev", {...}].

payload / args / request / result are JSON strings, exactly the text the native protocol carries, so both sides reuse their existing codecs. Modules the page implements (Host, Alert, Clipboard, ...) are called there; every other native module falls back to the Python implementations in pythonnative.native_modules.fallback.

Classes:

Name Description
WebTransport

Bridge transport whose native side is a browser page.

Attributes:

Name Type Description
BROWSER_MODULES

Native modules the preview page implements; the rest use Python fallbacks.

BROWSER_MODULES module-attribute

BROWSER_MODULES = frozenset(
    {
        "Host",
        "Alert",
        "Clipboard",
        "Linking",
        "Share",
        "Haptics",
        "NetInfo",
        "AppState",
        "Device",
    }
)

Native modules the preview page implements; the rest use Python fallbacks.

REQUEST_TIMEOUT_S module-attribute

REQUEST_TIMEOUT_S = 15.0

How long a synchronous request waits for the page before giving up.

WebTransport

WebTransport(
    *, log: Optional[Callable[[str], None]] = None
)

Bridge transport whose native side is a browser page.

Install it with pythonnative.bridge.set_transport and hand it to DevServer.set_preview_channel; the page does the rest.

Parameters:

Name Type Description Default
log Optional[Callable[[str], None]]

Where diagnostics go (defaults to stderr).

None

Methods:

Name Description
protocol_version

The page speaks whatever this package speaks; they ship together.

set_callback

Install the native -> Python entry point (bridge.native_callback).

apply

Forward one commit to the page (fire and forget).

measure

Ask the page for the intrinsic size of tag.

command

Run an imperative command on one view; returns its JSON result or None.

animate

Handle an animation request (set / start / cancel) for one view.

call

Call a native module: Host.post locally, page modules over the wire, others in Python.

post_to_main

Queue fn for the main loop (never runs inline).

run_main_loop

Drain main-thread work until stop is called (or until holds).

drain_main

Run queued main-thread work inline (tests); returns how many jobs ran.

stop

Make run_main_loop return.

on_preview_connected

A page connected; it becomes the native side.

on_preview_disconnected

The page went away: fail waiting requests and tear down its screens.

on_preview_message

Route one frame from the page.

send_dev

Send a ["dev", {...}] message to the page (logs, reload status, ...).

Attributes:

Name Type Description
on_dev_message Optional[Callable[[Dict[str, Any]], None]]

Hook for ["dev", {...}] messages from the page (runs on the main thread).

on_peer_changed Optional[Callable[[bool], None]]

Called on the main thread with True on connect and False on disconnect.

connected bool

Whether a page is attached.

on_dev_message instance-attribute

on_dev_message: Optional[
    Callable[[Dict[str, Any]], None]
] = None

Hook for ["dev", {...}] messages from the page (runs on the main thread).

on_peer_changed instance-attribute

on_peer_changed: Optional[Callable[[bool], None]] = None

Called on the main thread with True on connect and False on disconnect.

connected property

connected: bool

Whether a page is attached.

protocol_version

protocol_version() -> int

The page speaks whatever this package speaks; they ship together.

set_callback

set_callback(callback: Callback) -> None

Install the native -> Python entry point (bridge.native_callback).

apply

apply(transaction_json: str) -> None

Forward one commit to the page (fire and forget).

measure

measure(
    tag: int, max_width: float, max_height: float
) -> Tuple[float, float]

Ask the page for the intrinsic size of tag.

command

command(
    tag: int, name: str, args_json: str
) -> Optional[str]

Run an imperative command on one view; returns its JSON result or None.

animate

animate(tag: int, request_json: str) -> Optional[str]

Handle an animation request (set / start / cancel) for one view.

call

call(
    module: str, method: str, args_json: str
) -> Optional[str]

Call a native module: Host.post locally, page modules over the wire, others in Python.

post_to_main

post_to_main(fn: Callable[[], None]) -> None

Queue fn for the main loop (never runs inline).

run_main_loop

run_main_loop(
    *, until: Optional[Callable[[], bool]] = None
) -> None

Drain main-thread work until stop is called (or until holds).

This is the preview's event loop: the browser's stand-in for the platform main queue. Call it from the thread that should own the framework (the process main thread under pn preview).

drain_main

drain_main(timeout: float = 0.0) -> int

Run queued main-thread work inline (tests); returns how many jobs ran.

stop

stop() -> None

Make run_main_loop return.

on_preview_connected

on_preview_connected(
    peer: Any, info: Dict[str, Any]
) -> None

A page connected; it becomes the native side.

on_preview_disconnected

on_preview_disconnected(peer: Any) -> None

The page went away: fail waiting requests and tear down its screens.

on_preview_message

on_preview_message(peer: Any, text: str) -> None

Route one frame from the page.

send_dev

send_dev(payload: Dict[str, Any]) -> None

Send a ["dev", {...}] message to the page (logs, reload status, ...).

pythonnative.project.fingerprint

Native build fingerprints: know when pn run can skip the toolchain.

A debug build only has to be rebuilt when one of its native inputs changes: pythonnative.toml, the bundled native template, the pythonnative package itself, project-local native plugins, or the build flavor (platform, SDK, release). Edits under app/ don't count; the dev server syncs those into the running app and Fast Refresh applies them, exactly as Metro does for a React Native debug build.

compute hashes those inputs into one hex digest. pn run writes it next to the build after a successful toolchain run (see write_stamp) and, when the digest is unchanged and a dev server is running to deliver the latest sources, reinstalls the previous artifact instead of staging and compiling again.

The hash covers file contents, not mtimes, so touching a file or re-cloning the repository doesn't invalidate a build.

Functions:

Name Description
hash_tree

Hash every file under root (relative path + contents), deterministically.

compute

Digest every native input of a build.

read_stamp

Return the stamp written by the last successful build, or None.

write_stamp

Record fingerprint (and the artifact it produced) for the next pn run.

Attributes:

Name Type Description
STAMP_NAME

File written into build/<platform>/ after a successful native build.

STAMP_NAME module-attribute

STAMP_NAME = '.pn-native-fingerprint.json'

File written into build/<platform>/ after a successful native build.

hash_tree

hash_tree(
    root: Path, *, into: Optional["hashlib._Hash"] = None
) -> str

Hash every file under root (relative path + contents), deterministically.

Build outputs, caches, and bytecode are skipped so a checkout that has been built hashes the same as a fresh one.

Parameters:

Name Type Description Default
root Path

Directory (or single file) to hash. A missing path hashes as the empty tree.

required
into Optional['hashlib._Hash']

An existing hasher to feed instead of creating one.

None

Returns:

Type Description
str

The hex digest (of into when given).

compute

compute(
    config: AppConfig,
    platform: str,
    *,
    template_root: Path,
    lib_root: Path,
    release: bool = False,
    ios_sdks: Sequence[str] = (),
    extra: Optional[Dict[str, str]] = None
) -> str

Digest every native input of a build.

Parameters:

Name Type Description Default
config AppConfig

The loaded project configuration (its file is hashed).

required
platform str

"android" or "ios".

required
template_root Path

The bundled native template directory.

required
lib_root Path

The pythonnative package directory that gets bundled.

required
release bool

Release builds are distinct from debug builds.

False
ios_sdks Sequence[str]

The iOS SDK slices being staged.

()
extra Optional[Dict[str, str]]

Additional key/value inputs (host arch, tool versions).

None

Returns:

Type Description
str

A hex SHA-256 digest.

read_stamp

read_stamp(build_dir: Path) -> Optional[Dict[str, str]]

Return the stamp written by the last successful build, or None.

write_stamp

write_stamp(
    build_dir: Path,
    fingerprint: str,
    *,
    artifact: Optional[Path] = None
) -> None

Record fingerprint (and the artifact it produced) for the next pn run.

Next steps