Hot reload¶
Hot reload turns your edit-save-rebuild loop into edit-save-see. The
pn CLI watches app/ for changes and pushes the modified files
straight to the running app, where a small device-side helper reloads
the affected modules and asks the screen host to re-render.
Turn it on¶
Add --hot-reload to your pn run invocation:
pn will:
- Build and install the app once (the standard
runflow). - Launch the app on a connected device or simulator.
- Start a
FileWatcheroverapp/. - Push changed Python files into a writable on-device overlay.
- Write a small reload manifest that the running app polls from the main thread.
- Tail logs (Android) or print hot-reload notifications (iOS) until
you press
Ctrl+C.
How the device sees changes¶
The native templates call
configure_dev_environment()
before importing your app. That creates a pythonnative_dev/ directory
in the app's writable sandbox and puts it before the bundled app code
on sys.path.
When a source file changes, the CLI copies it to that overlay:
- Android: app-private storage via
adb+run-as - iOS Simulator: the installed app's
Documents/pythonnative_dev/directory
After the files are in place, the CLI writes reload.json. The
Android and iOS templates poll that manifest on the platform main
thread and call the screen host's reload hook. The host re-imports the
root component by dotted path, resets hook/navigation state for the
page, and mounts the refreshed tree.
What gets reloaded¶
PythonNative reloads any .py file under app/. The device-side
ModuleReloader resolves
the file to a dotted module name (e.g., app/pages/home.py becomes
app.pages.home) and re-imports it from disk.
After reloading, every active screen host runs Fast Refresh in place:
- Walk the live VNode tree and collect every component function defined in a reloaded module.
- Look up each function's replacement by
__module__+__qualname__in the freshly reloaded module (unwrapping the@pn.componentdecorator). - Rewrite the
Element.typereferences on every VNode in place — the next reconcile sees the new function with the sameHookState, so state survives.
The next render runs through
Reconciler.reconcile
just like a normal re-render, so layout and native views are
updated incrementally. Component state (use_state, use_reducer,
refs) is preserved across the swap.
If Fast Refresh can't find a clean swap — for example, a
component's __qualname__ changed, a new module was added that the
tree doesn't reference yet, or the swap raises — the host
falls back to a full remount of its root component so you never
get stuck with a stale tree. Hook state is reset in that case.
Per-screen scope: each native screen (UIViewController on iOS, ScreenFragment on Android) runs its own host, so Fast Refresh operates independently per host. Two pushed screens both running Fast Refresh for the same changed module each swap their own references.
What doesn't reload¶
- Native template files (anything under
android_template/orios_template/). Changes there require a full rebuild because the Java/Swift code is compiled into the app binary. - Files outside
app/. If you have a shared library next to your project, copy or symlink it underapp/to pick up changes. - C extension modules. Hot reload only updates Python source files;
recompiled
.so/.dyliblibraries are not re-loaded mid-session.
Common pitfalls¶
Top-level side effects
Code that runs at import time (e.g., a global registry that registers itself when the module is imported) runs again on every reload. Idempotent registration is fine; non-idempotent setup (counters, network calls) needs guarding.
References across modules
If module a does from b import Foo and only b.py changes,
module a may still hold the old Foo. The screen host always
reloads the root screen module after changed modules so common
component imports update, but long-lived references (e.g., stashed
in a global) can drift. When in doubt, restart the app.
Hook signature changes
Adding or removing a hook in a component changes the slot layout. Fast Refresh will swap the function in place but the next render can read the wrong slots, so the host falls back to a full remount when it detects the swap raises. If you see suspicious state after a hook-shape edit, close and reopen the affected screen (or restart the app) to clear the slate.
Renaming a component
Fast Refresh keys on each function's __qualname__. Renaming a
component changes the key, so the live VNode keeps its old
function until the parent re-renders with the new name. In
practice this means you may need to trigger one navigation or
state change for the renamed component to take effect; closing
and reopening the screen always works.
Working without --hot-reload¶
Hot reload is opt-in. If you'd rather rebuild on every change (more
predictable, slower), use pn run android / pn run ios without the
flag, or split the loop:
pn run android --prepare-only # stage files, skip the build
# ...edit...
pn run android # rebuild and re-install
--prepare-only is also useful for iterating on
AndroidManifest.xml or Info.plist because those changes never
hot-reload and you want the smallest possible cycle.
Reading device logs¶
Hot reload streams logs by default so you can see exceptions from your
reloaded modules. Pass --no-logs to suppress the stream:
On iOS hot reload currently targets the Simulator flow. Use Console.app or Xcode for full live logs while the CLI keeps the watcher process in the foreground.
Next steps¶
- Reference: Hot reload API.
- See where hot reload sits in the run loop:
pn run.