Skip to content

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.component functions return immutable Element descriptors, typed as pn.Node. Nothing is mounted until the Reconciler commits 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 Store for 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:

  1. Render: component functions run; hooks record state reads, queue effects, register memos. No native widgets change yet.
  2. 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.
  3. Effect: pending use_effect callbacks 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 pn CLI 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