Skip to content

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 apply_reload did.

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 changed_modules once and refresh every mounted screen.

Attributes:

Name Type Description
DEV_ROOT_DIR

Name of the writable on-device directory that shadows bundled app code.

DEV_ROOT_DIR module-attribute

DEV_ROOT_DIR = 'pythonnative_dev'

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 module_names in order, returning the names that succeeded.

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 {old_function: new_function} for one tree.

swap_components_in_tree

Apply a {old: new} map to every node in the reconciler tree.

refresh_in_place

Try a state-preserving Fast Refresh for one reconciler.

reload_module staticmethod

reload_module(module_name: str) -> bool

Reload a single module by its dotted name.

Parameters:

Name Type Description Default
module_name str

Dotted module name (e.g., "app.main").

required

Returns:

Type Description
bool

True if the module imported successfully from the current

bool

sys.path; False otherwise (the previous module object is

bool

restored so the app keeps running).

reload_modules staticmethod

reload_modules(module_names: Sequence[str]) -> List[str]

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_reload_targets(
    changed_modules: Sequence[str], component_path: str
) -> List[str]

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:

  1. Explicitly changed modules first (in the order given), so their fresh source replaces the cached version in sys.modules before any dependent modules re-execute.
  2. 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.
  3. The entry-point module itself, last, so its from ... import bindings 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 ("app.main") or a dotted attribute path ("app.main.RootScreen").

required

Returns:

Type Description
List[str]

The ordered list of modules to feed to

List[str]

file_to_module staticmethod

file_to_module(
    file_path: str, base_dir: str = ""
) -> Optional[str]

Convert a file path to a dotted module name.

Parameters:

Name Type Description Default
file_path str

Path to a .py file (absolute or relative).

required
base_dir str

Base directory that names should be relative to. If empty, file_path is treated as already relative.

''

Returns:

Type Description
Optional[str]

The dotted module name (e.g., "app.screens.home"), or

Optional[str]

None for an empty path.

modules_from_files staticmethod

modules_from_files(
    file_paths: Sequence[str], base_dir: str = ""
) -> List[str]

Convert Python source paths to importable module names.

find_replacement_function staticmethod

find_replacement_function(old_fn: Any) -> Optional[Any]

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 Element's type slot.

required

Returns:

Type Description
Optional[Any]

The reloaded module's matching function, None if no

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

build_replacement_map(
    reconciler: Any, reloaded_modules: Iterable[str]
) -> Dict[Any, Any]

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 root should be inspected.

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

swap_components_in_tree(
    reconciler: Any, replacement_map: Dict[Any, Any]
) -> int

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

refresh_in_place(
    reconciler: Any, reloaded_modules: Iterable[str]
) -> bool

Try a state-preserving Fast Refresh for one reconciler.

Returns:

Type Description
bool

True if any component function was replaced (callers

bool

should then trigger a re-render). False means the

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

"fast_refresh" when every host refreshed in place, "remount" when at least one fell back to a full remount, "error" when a host hit an exception (shown in its RedBox), or "none" when nothing could be reloaded.

error Optional[str]

The import error text when a changed module failed to execute (the previous module stays in sys.modules).

hosts int

Number of hosts refreshed.

configure_dev_environment

configure_dev_environment(
    writable_root: str, server_url: Optional[str] = None
) -> str

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 filesDir, iOS Documents, or a test directory).

required
server_url Optional[str]

The dev server this launch should connect to, when the launcher passed one (pn run does, through a launch environment variable on iOS and an intent extra on Android). It is exported as PN_DEV_SERVER so the dev client picks it up; a remembered server is used otherwise.

None

Returns:

Type Description
str

Absolute path to the overlay root.

overlay_root

overlay_root() -> Optional[str]

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 (pythonnative.hosts.live_hosts).

None

Returns:

Type Description
ReloadResult

Next steps