Fast Refresh¶
Fast Refresh turns the edit-save-rebuild loop into edit-save-see. The
dev server watches app/, pushes each changed file
to every connected client (the browser preview, simulators, emulators,
phones), and each client reloads the affected modules and refreshes its
mounted screens in place, keeping component state.
There's nothing to turn on. pn start (or pn preview) is the server;
any debug build launched with pn run while it's running is a client.
What happens on save¶
- The server's watcher notices
app/screens/home.pychanged, updates its manifest, and broadcasts anupdatewith the new contents. - Each client writes the file into its overlay, a writable
directory that sits ahead of the bundled sources on
sys.path(the browser preview has no overlay; the project directory is already on its path). - The client resolves the path to a module (
app.screens.home) and callsapply_reloadon the application thread. apply_reloadre-executes the changed module, then the other imported modules underappthat may hold bindings to it (the entry module'sfrom app.screens.home import HomeScreen, for instance), leaves first.- The application host runs Fast Refresh: it walks its VNode
tree, finds each component function whose module was reloaded,
looks up the replacement by
__module__+__qualname__, and rewrites theElement.typereferences in place. The next reconcile sees the new function with the sameHookState, so state survives when the captured hook signature is compatible. Hook-order or custom-hook changes remount the affected component, and a changed class definition remounts the application (see When state survives). - The host re-renders. Layout and native views update incrementally through the normal reconciler path.
- The client reports back (
fast_refresh: app.screens.home 42ms), which prints in thepn startterminal and toasts in the preview.
If Fast Refresh can't find a clean swap (a component's __qualname__
changed, a render raised with the new function, or the swap itself
failed), the host falls back to a full remount of its root, so you
never get stuck with a stale tree. Hook state is reset in that case
and the report says remount. When no screen is mounted yet, nothing is
refreshed and nothing is reported; the first render reads the new files.
If the saved file fails to import (a syntax error mid-edit), the
previous module stays in sys.modules, the traceback shows in the
RedBox and the terminal, and the app keeps running. Fix the file and
save again.
Refresh walks the application's shared logical tree, including covered screens and mounted list rows. Native containers don't create separate Python hosts.
When state survives¶
A save keeps component state unless one of the following is true:
- A component's hooks changed. Fast Refresh records each component's hook calls (their order, kind, and binding names, following custom hooks defined in your app). If the new function's signature differs, only the instances of that component remount; the rest of the tree keeps its state.
- A class definition changed. Before reloading,
apply_reloadfingerprints every class defined in each module it's about to re-execute. The fingerprint covers the class's name, metaclass, and bases; its annotated field names; its dataclass fields and their defaults; its immutable class attributes (numbers, strings, tuples, andEnummember values); and the bytecode, constants, names, and defaults of every method, property,staticmethod, andclassmethod. If a class that existed before is removed, renamed, or has a different fingerprint after the reload, the whole application remounts, because an instance of the old class could still sit inuse_state,use_memo, oruse_ref.
Everything else preserves state. In particular, these never force a remount:
- Editing a component's body, text, styles, or props (as long as its hooks stay the same).
- Adding, editing, or removing plain functions, lambdas, and constants. They're rebound when the module re-executes, and components pick them up on their next render.
- Defining or editing a
TypedDictorProtocol. They describe shapes; nothing holds an instance of the class itself. - Re-executing a module whose classes didn't change. A dataclass,
Enum,NamedTuple, or exception class that's identical to its previous definition keeps its fingerprint. - Moving a class, adding blank lines or comments around it, or editing the text of its docstring or its methods' docstrings. Line numbers, file names, and docstring text aren't part of the fingerprint.
- Adding a new class. No live instance can refer to it yet.
Unchanged classes are still new objects
Re-executing a module creates new class objects even when their
fingerprints match. An instance created before the save keeps its old
class, so isinstance checks against the new class, or identity
comparisons between an old Enum member held in state and a new one,
return False. Compare Enum members by .name or .value if that
matters during development, or reload the app.
What gets reloaded¶
Any .py file under app/. Assets under app/ (images, JSON, fonts)
are synced too, so an Image that points at a bundle-relative file
picks up the new bytes the next time it renders.
What doesn't reload¶
- Native template files (anything under
android_template/orios_template/) and native plugins. Changes there require a rebuild;pn rundetects them through the native fingerprint and runs the toolchain automatically. pythonnative.toml. Permissions, requirements, and app metadata are native inputs;pn runrebuilds when it changes.- 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. Recompiled
.so/.dyliblibraries are not reloaded mid-session. - The
pythonnativepackage itself. Reinstall and rebuild.
Common pitfalls¶
Top-level side effects
Code that runs at import time (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, opening files) needs guarding.
References across modules
If module a does from b import Foo and only b.py changes,
a is re-executed too so its binding updates, but long-lived
references stashed elsewhere (a module-level cache, an object held
in use_ref) can drift. When in doubt, use Reload app in the
preview or relaunch the device build.
Hook signature changes
Adding or removing a hook in a component changes the slot layout. Fast Refresh compares captured hook signatures before preserving state. Hook-order and custom-hook changes remount affected component instances. Changed class definitions remount the application; see When state survives.
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. Trigger a
navigation or state change, or reload the app.
Without a dev server¶
Fast Refresh needs pn start running. If you pn run without one, the
CLI says so and builds an app that runs its bundled sources; start the
server and relaunch to connect it. For rebuild-on-every-change (more
predictable, much slower), pass --rebuild.
The app also needs your dev token, which pn run passes along with the
server's URL; see The dev token. If
you delete the token file, relaunch installed debug builds with pn run
so they pick up the new one.
Release builds have no Fast Refresh at all: they leave out
pythonnative.hot_reload, pythonnative.refresh, and the dev client.
See Building for release.
Reading logs¶
Every dev client mirrors its print output, warnings, and tracebacks
to the pn start terminal, so you rarely need a device log viewer. For
native-level output, pn logs ios / pn logs android attach to
os_log and logcat; pn run does the same after launching unless
you pass --no-logs.
Next steps¶
- The whole loop: Development workflow.
- Reference: Hot reload API and Dev server API.