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. Nothing is mounted until the Reconciler commits the tree.
  • Hooks (use_state, use_effect, etc.) drive re-renders. State updates are batched per render pass.
  • Re-rendering produces a new tree; the reconciler diffs it against the previous one and applies the smallest set of native mutations.
  • 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 the registered ViewHandler implementations.
  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 (with a safety cap that prevents render storms).

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; debug builds and the --dev-client shell connect over WebSocket; Fast Refresh reloads .py modules in place
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 via a custom ViewHandler.

Next steps