Hot reload¶
Hot-reload comes in two cooperating pieces: a host-side file watcher
that pushes changed .py files to the device, and a device-side
module reloader that swaps the new code in and re-renders the active
page. Both are wired up automatically by pn run --hot-reload.
Hot-reload support for PythonNative development.
Two cooperating pieces:
- Host-side:
FileWatcherpolls the developer'sapp/directory for.pychanges and triggers a callback (typicallyadb pushon Android or asimctlfile copy on iOS). - Device-side:
ModuleReloaderreloads changed Python modules usingimportliband asks the screen host to re-render its current tree.
Two strategies share the device-side surface:
- Fast Refresh (default): after reloading the changed modules
the reconciler tree is walked and every component function whose
module was reloaded is swapped in place. Hook state, navigation
state, and even scroll positions survive because the underlying
VNodeobjects are reused — the next render simply calls the new function bodies through the old slots. - Full remount: when the in-place swap fails (e.g. the new module raised at import time, or a render exception bubbled out while running the new function), the host falls back to building a brand-new reconciler tree. State is lost but the app keeps running.
Example
Integrated into pn run --hot-reload:
Classes:
| Name | Description |
|---|---|
FileWatcher |
Watch a directory tree for |
ModuleReloader |
Reload changed Python modules on device and trigger a re-render. |
Functions:
| Name | Description |
|---|---|
configure_dev_environment |
Create and prioritize the writable hot-reload source overlay. |
manifest_path_for |
Return the reload-manifest path inside a hot-reload overlay. |
Attributes:
| Name | Type | Description |
|---|---|---|
DEV_ROOT_DIR |
Name of the writable on-device directory that shadows bundled app code. |
|
RELOAD_MANIFEST |
Manifest filename written by the host and polled by native templates. |
DEV_ROOT_DIR
module-attribute
¶
Name of the writable on-device directory that shadows bundled app code.
RELOAD_MANIFEST
module-attribute
¶
Manifest filename written by the host and polled by native templates.
FileWatcher
¶
Watch a directory tree for .py file changes.
Uses simple os.path.getmtime polling rather than a native
inotify/FSEvents binding so the watcher works on every platform
where Python runs without extra dependencies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
watch_dir
|
str
|
Directory to watch (recursively). |
required |
on_change
|
Callable[[List[str]], None]
|
Called with a list of changed file paths when modifications are detected. |
required |
interval
|
float
|
Polling interval, in seconds. |
1.0
|
Attributes:
| Name | Type | Description |
|---|---|---|
watch_dir |
Directory being watched. |
|
on_change |
Change callback. |
|
interval |
Polling interval. |
Methods:
| Name | Description |
|---|---|
start |
Begin watching in a background daemon thread. |
stop |
Stop the watcher and join the background thread. |
ModuleReloader
¶
Reload changed Python modules on device and trigger a re-render.
Designed to be invoked from device-side glue when a hot-reload
push completes. All public methods are static; the class holds a
single piece of process-wide state — the manifest version that
has most recently been applied to sys.modules — so that
multiple screen hosts polling the same manifest do not each
re-execute the user-app modules. The first host to see a new
version pays the reload_modules cost; subsequent hosts on the
same version refresh only their own reconciler tree against the
already-fresh modules.
Methods:
| Name | Description |
|---|---|
reload_module |
Reload a single module by its dotted name. |
reload_modules |
Reload the modules that are already imported. |
reload_modules_for_version |
Reload |
expand_reload_targets |
Expand a manifest of changed modules into the full reload order. |
file_to_module |
Convert a file path to a dotted module name. |
modules_from_files |
Convert Python source paths to importable module names. |
reload_screen |
Force a screen re-render after a module reload. |
find_replacement_function |
Locate a function's post-reload counterpart by qualname. |
build_replacement_map |
Compute |
swap_components_in_tree |
Apply a |
refresh_in_place |
Try a state-preserving Fast Refresh for one reconciler. |
reload_from_manifest |
Apply a reload manifest if it is newer than |
reload_module
staticmethod
¶
reload_modules
staticmethod
¶
reload_modules_for_version
staticmethod
¶
Reload module_names for version, deduping across hosts.
Each native screen host on iOS / Android runs its own poll
loop and would otherwise call
reload_modules
independently for the same manifest version. That re-executes
every user-app module N times (once per host) per file change,
producing N different generations of the same function objects
in sys.modules and leaving each host's reconciler tree
pointing at a different generation. Beyond the wasted work,
the inconsistent state has been observed to crash UIKit on iOS
with CALayerInvalidGeometry (NaN values fed into setFrame_:
during the interleaved renders).
This helper serializes on
_reload_lock and uses
_last_reloaded_version
to ensure only the first host to see a given version
actually re-executes the modules. Subsequent hosts on the same
version get back the already-fresh entries from sys.modules
so their own
refresh_in_place
pass can still rewrite their tree against the same generation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module_names
|
Sequence[str]
|
Dotted module names to reload. |
required |
version
|
Optional[str]
|
Manifest version this reload is processing. When
|
required |
Returns:
| Type | Description |
|---|---|
List[str]
|
The list of module names that are currently fresh in |
List[str]
|
|
List[str]
|
already reloaded by an earlier host for the same version. |
expand_reload_targets
staticmethod
¶
Expand a manifest of changed modules into the full reload order.
When a user edits app/screens/home.py, only that file is in
the manifest. But the entry-point module app.main has
bindings like from app.screens.home import HomeScreen that
need to be re-evaluated against the freshly-loaded
app.screens.home; likewise other user-app modules may carry
transitive bindings (e.g. through a shared app/theme.py)
that go stale if only the changed file is reloaded.
This helper computes the full ordered reload list:
- Explicitly changed modules first (in the order given), so
their fresh source replaces the cached version in
sys.modulesbefore any dependent modules re-execute. - All other currently-imported modules under the entry-point's top-level package, deepest first. The depth heuristic biases toward leaves so re-executing a screen file picks up the newest shared utilities before the file that imports it does.
- The entry-point module itself, last, so its
from ... importbindings rebind against everything that was refreshed in steps 1 and 2.
Modules outside the entry-point's top-level package
(pythonnative.*, stdlib, third-party) are never included;
framework code is not reloaded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
changed_modules
|
Sequence[str]
|
Modules reported as changed by the host file-watcher (already in dotted form). |
required |
component_path
|
str
|
The host's entry-point identifier, either a
module path ( |
required |
Returns:
| Type | Description |
|---|---|
List[str]
|
The ordered list of modules to feed to |
List[str]
|
file_to_module
staticmethod
¶
Convert a file path to a dotted module name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path
|
str
|
Path to a |
required |
base_dir
|
str
|
Base directory that names should be relative to.
If empty, |
''
|
Returns:
| Type | Description |
|---|---|
Optional[str]
|
The dotted module name (e.g., |
Optional[str]
|
|
modules_from_files
staticmethod
¶
Convert Python source paths to importable module names.
reload_screen
staticmethod
¶
Force a screen re-render after a module reload.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
screen_instance
|
Any
|
A |
required |
module_names
|
Optional[Sequence[str]]
|
Optional modules that changed. Reload-aware screen hosts use this to refresh imports before re-render. |
None
|
find_replacement_function
staticmethod
¶
Locate a function's post-reload counterpart by qualname.
Functions decorated with component
store the user's original function on the wrapper's
__wrapped__ attribute and forward __module__ /
__qualname__ so that the reconciler's stored
element.type (the unwrapped function) still has the
information needed to re-resolve after a module reload.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_fn
|
Any
|
The function captured in an
|
required |
Returns:
| Type | Description |
|---|---|
Optional[Any]
|
The reloaded module's matching function, |
Optional[Any]
|
replacement was found, or the original function itself |
Optional[Any]
|
when the module has not been reloaded (so callers can |
Optional[Any]
|
skip the swap). |
build_replacement_map
staticmethod
¶
Compute {old_function: new_function} for one tree.
The reconciler's stored tree references the pre-reload
component functions through VNode.element.type. This
method walks the tree, collects every callable type whose
__module__ was just reloaded, and asks
find_replacement_function
for its successor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reconciler
|
Any
|
The reconciler whose
|
required |
reloaded_modules
|
Iterable[str]
|
Set of module names that were just reloaded (only callables from these modules are considered). |
required |
Returns:
| Type | Description |
|---|---|
Dict[Any, Any]
|
A mapping suitable for passing to |
Dict[Any, Any]
|
swap_components_in_tree
staticmethod
¶
Apply a {old: new} map to every node in the reconciler tree.
Mutates vnode.element.type directly so the NEXT diff sees
identical types and reuses VNodes (preserving hook state).
Pending Element trees stored on vnode._rendered are
rewritten too because the reconciler reads from them when
comparing keys across renders.
Returns:
| Type | Description |
|---|---|
int
|
The number of element type references that were rewritten. |
refresh_in_place
staticmethod
¶
Try a state-preserving Fast Refresh for one reconciler.
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
should then trigger a re-render). |
bool
|
tree already references the latest functions (or has no |
bool
|
nodes from the reloaded modules at all). |
reload_from_manifest
staticmethod
¶
reload_from_manifest(screen_instance: Any, manifest_path: str, *, last_version: Optional[str] = None) -> Optional[str]
Apply a reload manifest if it is newer than last_version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
screen_instance
|
Any
|
Screen host to refresh. |
required |
manifest_path
|
str
|
JSON manifest written by the CLI. |
required |
last_version
|
Optional[str]
|
Version already applied by this screen host. |
None
|
Returns:
| Type | Description |
|---|---|
Optional[str]
|
The manifest version after applying, or |
Optional[str]
|
no new manifest is available. |
configure_dev_environment
¶
Create and prioritize the writable hot-reload source overlay.
The returned directory is inserted at the front of sys.path, so a
pushed app/main.py shadows the copy bundled into the native
application. Templates call this before importing user code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
writable_root
|
str
|
Platform data directory that the app can write to
(Android |
required |
Returns:
| Type | Description |
|---|---|
str
|
Absolute path to the hot-reload overlay root. |
Next steps¶
- See the workflow in Hot reload guide.