Architecture¶
PythonNative combines direct native bindings with a declarative reconciler, giving you React-like ergonomics while calling native platform APIs synchronously from Python.
High-level model¶
- Declarative element tree. Your
@pn.componentfunction returns a tree ofElementdescriptors (similar to React elements / virtual DOM nodes). - Function components and hooks. All UI is built with
@pn.componentfunctions usinguse_state,use_reducer,use_effect,use_navigation, and friends. The API is inspired by React hooks but designed for Python. - Reconciler. On first render, the
Reconcilerwalks the tree and creates real native views via the platform backend. On subsequent renders (triggered by hook state changes), it diffs the new tree against the previous one and applies the minimal set of native mutations. - Post-render effects. Effects queued via
use_effectare flushed after the reconciler commits native mutations, matching React semantics. This guarantees that effect callbacks interact with the committed native tree. - State batching. Multiple state updates triggered during a
render pass (e.g., from effects) are automatically batched into a
single re-render. Explicit batching is available via
batch_updates. - Key-based reconciliation. Children can be assigned stable
keyvalues to preserve identity across re-renders, which is critical for lists and dynamic content. - Error boundaries.
ErrorBoundarycatches render errors in child subtrees and displays fallback UI, preventing a single component failure from crashing the entire page. - Direct bindings. Under the hood, native views are created and updated through direct platform calls:
- iOS: rubicon-objc exposes Objective-C/Swift classes
(
UILabel,UIButton,UIStackView, etc.). - Android: Chaquopy exposes Java classes
(
android.widget.TextView,android.widget.Button, etc.) via the JNI bridge. - Thin native bootstrap. The host app remains native (Android
Activityor iOSUIViewController). It callscreate_screeninternally to bootstrap your Python component, and the reconciler drives the UI from there. Appentry point. The user's app module (app/main.py) defines a top-level component namedApp. Native templates import that module by path ("app.main") and look up itsAppattribute, so users never write a separate registration step. Components with other names can still be loaded by passing an explicit dotted path like"app.main.RootScreen"to the template.
How it works¶
@pn.component fn ---> Element tree ---> Reconciler ---> Native views
^ |
| v
set_state() <--------- schedule re-render batched ---> diff + patch
|
v
flush effects
The reconciler uses key-based diffing: children are matched by key first and by position only as a fallback. When a child with the same key and type is found, its props are updated in place on the native view. When the type changes, the old native view is destroyed and a new one is created.
Render lifecycle¶
- Render phase: component functions execute. Hooks record state reads, queue effects, and register memos. No native mutations happen yet.
- Commit phase: the reconciler applies the diff to native views, creating, updating, and removing views as needed.
- Effect phase: pending effects are flushed in depth-first order (children before parents). Cleanup functions from the previous render run before new effect callbacks.
- Drain phase: if effects set state, a new render pass is automatically triggered and the cycle repeats (up to a safety limit to prevent infinite loops).
See Lifecycle for a detailed walkthrough.
Component model¶
PythonNative uses a single component model: function components
decorated with @pn.component.
@pn.component
def Counter(initial: int = 0):
count, set_count = pn.use_state(initial)
return pn.Column(
pn.Text(f"Count: {count}", style={"font_size": 18}),
pn.Button("+", on_click=lambda: set_count(count + 1)),
style={"spacing": 4},
)
Each component is a Python function that:
- Accepts props as keyword arguments.
- Uses hooks for state (
use_state,use_reducer), side effects (use_effect), navigation (use_navigation), and more. - Returns an
Elementtree describing the UI. - Has its own hook state per call site (each instance gets its own slot table).
The entry point create_screen is called
internally by the bundled native templates to bootstrap your root
component. App code does not call it directly.
Styling¶
styleprop: pass a dict (or a list of dicts) to any component. For example,style={"font_size": 24, "color": "#333"}.- StyleSheet: create reusable named style dictionaries with
StyleSheet.createand compose them withStyleSheet.compose. - Theming: use
ThemeContextwithProvideranduse_contextto propagate theme values through the tree.
Layout¶
PythonNative ships its own pure-Python flexbox engine (a small,
React-Native-compatible re-implementation of Yoga's algorithm). All
layout decisions are made in Python and then pushed to native views as
absolute frames via set_frame. This means the exact same layout
rules apply on Android and iOS — there is no platform drift between
LinearLayout and UIStackView.
The engine is implemented in pythonnative.layout and runs as a
dedicated layout pass after every commit:
render -> commit (create / update native views)
-> flush effects
-> build LayoutNode tree from VNodes
-> calculate_layout(viewport_w, viewport_h)
-> backend.set_frame(view, x, y, w, h) for every node
View is the universal flex container (like
React Native's View). It defaults to flex_direction: "column".
Column and Row are
convenience wrappers that fix the direction.
Flex container properties (inside style)¶
flex_direction:"column"(default),"row","column_reverse","row_reverse".justify_content: main-axis distribution:"flex_start","center","flex_end","space_between","space_around","space_evenly".align_items: cross-axis alignment:"stretch","flex_start","center","flex_end".overflow:"visible"(default),"hidden".spacing: gap between children (dp / pt).padding: inner spacing.
Child layout properties¶
flex: shorthand forflex_grow: N, flex_shrink: 1, flex_basis: 0.flex_grow,flex_shrink,flex_basis: individual flex properties.align_self: override the parent'salign_itemsfor this child.width,height: fixed dimensions (numbers or"%"strings).min_width,min_height,max_width,max_height: size constraints.aspect_ratio: derive the unknown axis from the known one.margin: outer spacing.position:"relative"(default) or"absolute". Absolute children are removed from the flex flow and positioned viatop/right/bottom/left.
Under the hood:
- Layout:
pythonnative.layout.calculate_layoutcomputes a frame(x, y, w, h)for every node. - Android: every container is a
FrameLayout; computed frames are applied throughMarginLayoutParamsandView.setX/setY/setLayoutParams. - iOS: every container is a plain
UIViewwithtranslatesAutoresizingMaskIntoConstraints = NO; computed frames are applied throughview.frame = CGRect(...). - Intrinsic content size: leaf widgets (
Text,Button,Image,TextInput, …) implementmeasure_intrinsicso the engine can ask them how big they want to be when no explicit size is set.
See the Layout engine concept page for a full walkthrough.
Native view handlers¶
Platform-specific rendering logic lives in the
pythonnative.native_views package, organized into dedicated
submodules:
native_views.base: sharedViewHandlerprotocol and common utilities (color parsing, padding resolution, container visual keys).native_views.android: Android handlers using Chaquopy's Java bridge (jclass,dynamic_proxy).native_views.ios: iOS handlers using rubicon-objc (ObjCClass,objc_method).
Every handler implements two layout-facing methods:
set_frame(view, x, y, width, height)— apply an absolute frame computed by the layout engine.measure_intrinsic(view, max_width, max_height)— return the natural content size for leaf widgets (used as a hint by the layout engine).
Column, Row, and View share a single flex-container handler on
each platform. Containers are simple FrameLayout (Android) /
UIView (iOS) instances; all flex math lives in
pythonnative.layout, so the handlers themselves contain no layout
logic.
Each handler class maps an element type name (e.g., "Text",
"Button") to platform-native widget creation, property updates, and
child management. The
NativeViewRegistry
lazily imports only the relevant platform module at runtime, so the
package can be imported on any platform for testing.
Comparisons¶
Versus React Native
React Native uses JSX plus a JavaScript bridge (or JSI in newer versions) plus Yoga layout. PythonNative uses Python plus direct native calls plus a Python-implemented Yoga-style flex engine; no JS bridge, no serialization overhead, and the same layout rules on both platforms.
Versus NativeScript
NativeScript shares the philosophy of direct, synchronous native access, but PythonNative adds a declarative reconciler layer and React-like hooks that NativeScript does not have by default.
See Mental model for a wider comparison table.
iOS flow (rubicon-objc)¶
- The iOS template (Swift plus PythonKit) boots Python and calls
create_screeninternally with the currentUIViewControllerpointer. - The reconciler creates UIKit views and attaches them to the controller's view.
- State changes trigger re-renders; the reconciler patches UIKit views in place.
Android flow (Chaquopy)¶
- The Android template (Kotlin plus Chaquopy) initializes Python in
MainActivityand passes theActivityto Python. ScreenFragmentcallscreate_screeninternally, which renders the root component and attaches views to the fragment container.- State changes trigger re-render; the reconciler patches Android views in place.
Hot reload (Fast Refresh)¶
During development, pn run --hot-reload watches app/ for file
changes and pushes updated Python files to the running app, enabling
near-instant UI updates without full rebuilds.
PythonNative uses a Fast Refresh strategy:
- Reload the changed module(s) on the device.
- For every active screen host, walk the VNode tree and collect every component function defined in a reloaded module.
- Match each one to its replacement by
__module__+__qualname__and rewriteElement.typein place. - Trigger one reconcile pass. Because the VNode and its
HookStateare reused, component state (use_state,use_reducer, refs) is preserved across the edit.
If Fast Refresh can't produce a clean swap, the host falls back to a full remount of its root component. See Hot reload guide.
Native API modules¶
PythonNative provides cross-platform modules for common device APIs:
Camera: photo capture and gallery picker.Location: GPS and location services.FileSystem: app-scoped file I/O.Notifications: local notifications.
See Native modules guide.
Navigation¶
PythonNative navigation is declarative and native-backed:
- The user describes their app as a tree of navigators
(
create_stack_navigator,create_tab_navigator,create_drawer_navigator) wrapped inNavigationContainer, and names the root componentAppso the native templates can find it. - The outermost
Stack.Navigatordelegatesnavigate(...),go_back(), andreset(...)to the platform's native navigation controller —UINavigationControlleron iOS and the AndroidX Navigation Component on Android. Nested navigators (tabs inside a stack, stacks inside tabs) stay in Python and reuse the existing reconciler. - Each pushed native screen is a fresh host with its own reconciler
and
_ScreenHost. Initial routes are forwarded via host arguments (__pn_initial_route__/__pn_initial_params__), so a pushed screen knows whichStack.Screento render on its first frame. - Inside any screen,
use_navigationreturns aNavigationHandle;use_routereturns the current route name and params. Both are the same hooks regardless of whether the active navigator is native-backed or pure-Python.
See the Navigation guide for the full
walkthrough, including how options={"title": ...} flows into the
native navigation bar.
- iOS: one host
UIViewControllerclass, many instances pushed on aUINavigationController. - Android: single host
Activitywith aNavHostFragmentand a stack of genericScreenFragments driven by a navigation graph.
Next steps¶
- Read the Mental model for the high-level comparisons.
- Walk through the render loop in Lifecycle.
- Dive into the flexbox engine in Layout engine.
- See the platform handlers up close in Native views.
- Browse the API: Package overview.