Mental model¶
PythonNative is a thin layer over native UIKit and Android view hierarchies, with a React-style component model on top. If you have worked with React Native, the surface API will feel familiar; the runtime, however, is meaningfully different.
TL;DR¶
@pn.componentfunctions return immutableElementdescriptors, typed aspn.Node. Nothing is mounted until theReconcilercommits the tree.- A component's signature is its prop list, so your type checker
verifies every call, including navigation: a screen's parameters are
its route params, and
nav.push(ItemScreen(id=42))is an ordinary call. - Hooks (
use_state,use_effect, etc.) drive re-renders. State updates are batched automatically: every setter call in one callback, effect, or task step produces one render pass. - Re-rendering produces a new tree; the reconciler diffs it against the previous one, skips subtrees whose elements didn't change, and applies the smallest set of native mutations.
- State lives in hooks for one component, in context for a subtree,
and in a
Storefor the app. See Managing state. - Native widgets are created and updated by Swift and Kotlin component managers that receive one serialized transaction per commit over the native bridge. Mobile applications embed CPython; the browser preview has its own JavaScript renderer.
The runtime in one diagram¶
@pn.component fn ---> Element tree ---> Reconciler ---> Native views
^ |
| v
set_state() <--------- schedule re-render batched ---> diff + patch
|
v
flush effects
Each render pass has three phases:
- Render: component functions run; hooks record state reads, queue effects, register memos. No native widgets change yet.
- Commit: the reconciler applies the diff to native views, creating, updating, and removing widgets through one serialized transaction that the Swift and Kotlin component managers apply.
- Effect: pending
use_effectcallbacks fire in depth-first order; cleanups from the previous render run before the new callbacks.
If an effect sets state, the loop kicks off again. A component that
keeps dirtying itself for more than fifty passes raises
RuntimeError("Too many re-renders"), routed through the nearest
ErrorBoundary like any render error.
How PythonNative differs from React Native¶
| Concept | React Native | PythonNative |
|---|---|---|
| Component language | JavaScript / TypeScript | Python |
| Bridge | Fabric: one C++ shadow tree commit per render; TurboModules for device APIs | One JSON transaction per commit applied by Swift / Kotlin component managers; named native modules for device APIs |
| Threading | UI runs on the main thread; JS on a separate thread | Native UI on its platform thread; Python on a dedicated asyncio application thread |
| Distribution | Metro bundler ships a JS bundle | pn build bundles your app/ and the pythonnative package into the native project |
| Dev loop | Metro dev server; Expo Go / dev client on device; Fast Refresh | pn start dev server with a QR code; PythonNative Go or the project's debug build connect over an authenticated WebSocket; Fast Refresh reloads .py modules in place |
| Dev tools | React Native DevTools, LogBox, the dev menu | DevTools served by pn start (components, console and REPL, problems, network, performance); the dev menu, element inspector, and performance monitor on device; debugpy breakpoints from VS Code |
| Navigation | navigation.navigate("Detail", {id}) with a typed ParamList |
nav.push(DetailScreen(id=42)); navigators are module-level values |
| Styles and themes | StyleSheet.create({...}); separate app and navigation themes |
class Styles(pn.StyleSheet) namespaces; one Theme dataclass for the app and its navigators |
| Global state | Zustand, Redux, or Jotai | pn.Store with pn.use_store(store, selector) |
| Rules of hooks | eslint-plugin-react-hooks |
pn lint |
| Preview without a device | Expo web / Snack | pn preview renders in a browser tab through the same bridge protocol the native runtimes speak |
| Native widgets | Wrapped by Fabric component managers | Wrapped by PNComponentManager (Swift) / ComponentManager (Kotlin) classes |
Python components run on a dedicated application thread. Native calls cross a versioned, serialized bridge and marshal widget operations to the platform UI thread. Native events are queued back to Python. Some module calls can return synchronously; asynchronous results and cancellation use the bridge's promise protocol. See The native bridge for the contract.
How PythonNative differs from Toga, BeeWare, Kivy¶
- Toga and Kivy are imperative widget toolkits. PythonNative is declarative: you describe the tree per render, and the reconciler figures out the diff.
- Kivy renders its own widgets via OpenGL. PythonNative renders real native widgets, so your buttons look like UIKit buttons on iOS and Material buttons on Android. Applications configure accessibility labels and roles and test interaction on each platform.
- BeeWare's Briefcase is a packaging story; PythonNative ships its
own
pnCLI for the same purpose, plus a UI runtime.
Mental shortcuts¶
When something feels surprising, fall back on these rules:
It's just Python
Application functions run in CPython. Native Swift, Kotlin, and C++
libraries are compiled when building the app.
print(x) reaches the device console; import foo runs the
bundled module. If a stack trace mentions pythonnative.*, you
can open that file in your editor and read the source.
Components are functions
Each call to a @component function should produce the same
output for the same input plus current hook state. Side effects
belong in use_effect.
Native widgets are real
A pn.Text becomes a UILabel or a TextView. Anything you can
do to those in their respective SDKs, you can usually do with a
custom native component:
a Swift and a Kotlin manager plus a
define_component call.
Next steps¶
- See the runtime in detail: Architecture.
- Walk through a render: Lifecycle.
- Learn how trees are diffed: Reconciliation.