| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [View Raw Code] [Original HTTPS Page] |
PythonNative's inner loop is modeled on Metro and Expo: one long-running dev server in your terminal, and any number of dev clients (a browser tab, simulators, emulators, physical phones) that connect to it. You edit a file, you save, and every connected client Fast Refreshes in place. Native builds happen only when something native changes.
┌────────────────────┐ ┌──────────────────────────────┐ │ pn start │ ws │ browser preview (tab) │ │ │◄──────►│ renders through the bridge │ │ watches app/ │ └──────────────────────────────┘ │ syncs sources │ ws ┌──────────────────────────────┐ │ streams logs │◄──────►│ iOS Simulator / Android │ │ │ │ emulator (pn run) │ │ │ ws ┌──────────────────────────────┐ │ │◄──────►│ phone on the same Wi-Fi │ └────────────────────┘ └──────────────────────────────┘
Terminal one runs the dev server for the whole session:
pn start # dev server only
pn preview # dev server and open the browser previewTerminal two builds and launches a debug app whenever you need one:
pn run ios
pn run androidpn run finds the dev server on localhost:8765 (--port to change it), bakes the server's URL into the debug build, installs it, and launches. The app connects on startup, pulls any sources newer than what it shipped with, and from then on Fast Refreshes on every save. Its print() output, tracebacks, and reload notices stream back to the pn start terminal, so you can leave pn run and keep working from one window.
Rerunning pn run is cheap: when nothing native changed since the last build (see When native rebuilds happen) it reinstalls the previous artifact and relaunches in a few seconds.
The dev server is one Python process, standard library only, doing three jobs:
Logs from every peer are interleaved in the terminal and prefixed with the source: [ios iPhone 15], [android Pixel 8], [browser], and [pn] for the server's own messages.
Endpoints, for scripting and curiosity:
| Path | Purpose |
|---|---|
| GET / | Browser preview page |
| GET /status | Server, project, and connected-peer info (JSON) |
| GET /manifest | {"version", "entry", "files": {path: sha256}} |
| GET /file/<path> | Raw bytes of one synced source file |
| WS /ws?role=client | Dev-client protocol |
| WS /ws?role=preview | Browser preview bridge channel |
pn start [entry] [--port 8765] [--host 0.0.0.0] [--open]
pn preview [entry] [--port 8765] [--host 0.0.0.0] [--no-open]entry overrides the entry module from pythonnative.toml (for example app.screens.settings to mount one screen's App). The server binds to all interfaces by default so phones on your network can reach it; pass --host 127.0.0.1 to keep it local.
A dev client is a debug build of your app. On launch, pythonnative.bootstrap.start(dev=True) calls [devclient.start_if_configured][pythonnative.devclient.start_if_configured], which:
Release builds never include any of this: pn build produces a standalone app with your sources bundled and no dev client.
pn run ios and pn run android handle the URL plumbing. The iOS Simulator shares the Mac's loopback interface, so localhost works. For Android the CLI runs adb reverse so localhost:8765 inside the emulator (or a USB-attached phone) reaches the server.
A physical iOS device is on your Wi-Fi rather than your loopback, so pn run ios --device <name> bakes in the Mac's first LAN address instead. Both machines must be on the same network and the port must not be firewalled. If auto-detection picks the wrong interface, pass the URL explicitly:
pn run ios --device "Owen's iPhone" --dev-server ws://192.168.1.20:8765/ws?role=clientFast Refresh works the same over Wi-Fi; there's no longer a USB-only path.
Sometimes you want one installed app that can load any project, the way Expo Go does. Build a shell with:
pn run ios --dev-client
pn run android --dev-clientThe shell has no app of its own. It opens a [ConnectScreen][pythonnative.devclient.ConnectScreen] where you type a dev server URL (the last one used is prefilled). After the first sync the real app.main from the overlay shadows the placeholder and the screen remounts into your app. The URL is remembered for next launch, so a shell built once keeps working across projects as long as their native inputs (native plugins, [requirements].packages, permissions) are the same.
A debug build has to go through Gradle or Xcode only when a native input changes:
pn run hashes the contents of all of those into one fingerprint (see [fingerprint.compute][pythonnative.project.fingerprint.compute]) and writes it next to the build after a successful toolchain run. On the next pn run, if the fingerprint matches and a dev server is up to deliver current sources, the previous artifact is reinstalled and launched. Edits under app/ never trigger a rebuild; they're synced.
Force the toolchain with --rebuild. Stage without building with --prepare-only (useful for opening the project in Xcode or Android Studio).
If no dev server is running when you pn run, the CLI says so and builds an app that runs its bundled sources without Fast Refresh. Start pn start and relaunch to connect it.
| Task | Use |
|---|---|
| Layout, state, navigation, most component work | Browser preview |
| Anything touching device APIs (camera, location, haptics, biometrics) | Simulator or device |
| Text rendering, fonts, platform chrome, gesture feel | Simulator or device |
| Performance | A physical device |
| Demoing to someone at a desk | Browser preview (--host and share the URL) |
The browser preview runs your real Python in the pn start process and shares Python reconciliation, hooks, and logical navigation with device builds. Its renderer uses Yoga WebAssembly and DOM widgets, and implements a subset of native modules (Alert, Clipboard, Linking, Share, Haptics, NetInfo, AppState, Device); everything else uses the pure-Python fallbacks in pythonnative.native_modules.fallback, so a Camera call returns an "unavailable" result rather than a photo.
Every dev client mirrors its Python output to the server, so the pn start terminal is the one place to watch. Uncaught exceptions in renders, effects, and handlers show a RedBox on the affected screen (on device and in the preview) and print the traceback in the terminal. Errors before any screen mounts (an ImportError in app/main.py, say) print in the terminal and pop up in the preview's console; fix the file and save to recover, nothing needs restarting.
pn logs ios / pn logs android still attach a native log stream (os_log, logcat) when you need lower-level output than the dev client mirrors.
The browser preview imports your app in the pn start process, so any package under [requirements].packages must be installed in the same Python environment (pip install it, or run uv run --with <pkg> pn start). pn start warns when one is missing. Device builds resolve their own wheels from the same list; see PyPI packages.
Set PN_PROFILE to a writable file path before starting the Python process:
PN_PROFILE=/tmp/pythonnative-trace.json pn previewCollection retains the latest 10,000 timing events and exports the trace at normal process exit. The trace includes render, layout, and commit phases, plus counters for rendered components and bridge operations and bytes. This command captures the preview's Python process; it doesn't configure an environment variable inside a connected mobile app.
For an explicit capture in a headless test:
import pythonnative as pn
from pythonnative.profiling import Profiler
from pythonnative.testing import render
@pn.component
def App():
return pn.Text("Profiled render")
with Profiler() as profiler:
result = render(App())
result.unmount()
profiler.export("trace.json")The same context manager can surround work on the application's Python thread in an embedded runtime. Choose an app-writable path when exporting on a device. Open the resulting Chrome trace in Perfetto or a compatible trace viewer. The profiler doesn't retain component trees or application snapshots. Use Instruments on iOS and Android's platform profiling tools for native frames, input, and scrolling.
| Back | FazBrowse Home | New Git URL |