Hot reload¶
Fast Refresh comes in two cooperating pieces: the
dev server that watches app/ and pushes changed files
to every connected client, and this device-side module reloader that
swaps the new code in and refreshes every mounted screen. Debug builds
launched with pn run while pn start is running are wired up
automatically.
Device-side module reloading for Fast Refresh.
The dev server (pythonnative.devserver) watches the project's
app/ directory and tells every connected dev client which files
changed. This module is the client's other half: it re-executes the
changed modules with importlib and refreshes the logical application tree.
Two strategies share the surface:
- Fast Refresh (default): after reloading the changed modules the reconciler tree is walked and every component function whose module was reloaded is matched to its replacement. Compatible hook signatures preserve state; hook-order or custom-hook changes remount the affected component instances. Covered screens and mounted rows participate in the same refresh.
- Full remount: changes to helper classes or services, or an unsuccessful component swap, rebuild the application tree. State is reset. A module import failure is reported while its previous definition remains available.
apply_reload is the single
entry point: it reloads once per process and then refreshes each live
application host.
On device, sources arrive in a writable overlay directory that
shadows the app bundle (see
configure_dev_environment);
under pn preview the project directory itself is on sys.path
and there is no overlay.
Classes:
| Name | Description |
|---|---|
ModuleReloader |
Reload changed Python modules and rewrite mounted trees to match. |
ReloadResult |
What |
Functions:
| Name | Description |
|---|---|
configure_dev_environment |
Create and prioritize the writable source overlay. |
overlay_root |
The overlay directory configured for this process, if any. |
apply_reload |
Reload |
Attributes:
| Name | Type | Description |
|---|---|---|
DEV_ROOT_DIR |
Name of the writable on-device directory that shadows bundled app code. |
DEV_ROOT_DIR
module-attribute
¶
Name of the writable on-device directory that shadows bundled app code.
ModuleReloader
¶
Reload changed Python modules and rewrite mounted trees to match.
All methods are static; the class is a namespace. The tree-rewrite
helpers (build_replacement_map, swap_components_in_tree,
refresh_in_place) are what make Fast Refresh state-preserving.
Methods:
| Name | Description |
|---|---|
reload_module |
Reload a single module by its dotted name. |
reload_modules |
Reload |
reload_module_strict |
Reload one module, propagating the import error instead of swallowing it. |
expand_reload_targets |
Expand a set 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. |
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_module
staticmethod
¶
Reload a single module by its dotted name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module_name
|
str
|
Dotted module name (e.g., |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
|
bool
|
restored so the app keeps running). |
reload_modules
staticmethod
¶
Reload module_names in order, returning the names that succeeded.
reload_module_strict
staticmethod
¶
reload_module_strict(module_name: str) -> None
Reload one module, propagating the import error instead of swallowing it.
Used by the dev client so a syntax error in a saved file shows up in the RedBox and the terminal rather than as a silent "nothing reloaded".
expand_reload_targets
staticmethod
¶
Expand a set of changed modules into the full reload order.
When a user edits app/screens/home.py, only that module is
reported. 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.
The order is:
- 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 (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.
find_replacement_function
staticmethod
¶
Locate a function's post-reload counterpart by qualname.
Component objects forward
__module__ / __qualname__ from the render function they
wrap, so the reconciler's stored element.type carries 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 mounted |
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.
Replaces immutable element descriptions so the next diff sees
identical types and reuses VNodes (preserving hook state).
The element lists 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). |
ReloadResult
dataclass
¶
ReloadResult(
requested: List[str] = list(),
reloaded: List[str] = list(),
mode: str = "none",
error: Optional[str] = None,
hosts: int = 0,
)
What apply_reload did.
Attributes:
| Name | Type | Description |
|---|---|---|
requested |
List[str]
|
Modules the caller reported as changed. |
reloaded |
List[str]
|
Modules actually re-executed (in reload order). |
mode |
str
|
|
error |
Optional[str]
|
The import error text when a changed module failed to
execute (the previous module stays in |
hosts |
int
|
Number of hosts refreshed. |
configure_dev_environment
¶
Create and prioritize the writable source overlay.
The returned directory is inserted at the front of sys.path, so a
synced app/main.py shadows the copy bundled into the native
application. Debug 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 |
server_url
|
Optional[str]
|
The dev server this launch should connect to, when
the launcher passed one ( |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Absolute path to the overlay root. |
overlay_root
¶
The overlay directory configured for this process, if any.
apply_reload
¶
apply_reload(
changed_modules: Sequence[str],
hosts: Optional[Sequence[Any]] = None,
) -> ReloadResult
Reload changed_modules once and refresh every mounted screen.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
changed_modules
|
Sequence[str]
|
Dotted module names whose source changed. |
required |
hosts
|
Optional[Sequence[Any]]
|
Screen hosts to refresh; defaults to every live host on
the current platform ( |
None
|
Returns:
| Type | Description |
|---|---|
ReloadResult
|
A |
Next steps¶
- See the workflow in the Fast Refresh guide.
- The other half: Dev server and dev client.