Skip to content

Architecture

PythonNative renders ordinary Python function components into native UIKit and Android widgets. A single logical tree connects the application root, providers, screens, overlays, and visible list rows.

flowchart LR
    A[Python components and state] --> B[Incremental reconciler]
    B -->|revisioned commit| C[Renderer core]
    C --> D[Validation, shadow tree, Yoga layout, flattening]
    D -->|binary mount transaction| E[UIKit, Android, or DOM widgets]
    E -->|events, pointers, container sizes| C
    C -->|stamped events, frames, command results| A
    F[Frame clock] --> C
    C -->|animated prop writes| E

The authoring model

Application code is ordinary, typed Python. Components are functions whose signatures are their prop lists, and they return a pn.Node, so a strict type checker verifies element trees, props, and conditional children. Screens are components whose parameters are their route params, and navigators are module-level values (see Navigation). One Theme styles both the app and its navigators, StyleSheet namespaces give styles typed names, and a Store holds app state outside the tree. pn lint checks the rules of hooks statically.

Run-time validation is a development aid. Built-in prop and style validation, and checks of user component props against their annotations, run only in development builds and under the pytest plugin. Release builds skip them and omit development-only modules (the CLI, dev server, Fast Refresh, browser preview, SDK code generators, and testing library) from the bundle.

Execution and ownership

runtime.py owns a standard asyncio application loop on a dedicated thread. Components, effects, and callbacks execute there. Component task scopes cancel work on unmount. Application services can own longer-lived task scopes. Blocking Python work delays application callbacks, so applications should use ordinary asyncio APIs and explicitly move blocking I/O off the application loop.

The reconciler maintains stable keyed instances, parent relationships, native tag indexes, dirty component work, and pending effects. Updating a child preserves its ancestors and siblings unless their own inputs or context change. A child element that's the identical object from the previous render is skipped with its subtree, and a changed context value marks only the consumers recorded in its provider's registry (see Reconciliation). Native child relationships are rebuilt when a component's native roots change. Effects run after the renderer core accepts and lays out a commit, with child effects preceding parents. Renders are batched automatically: setter calls made in one callback, effect, or task step coalesce into one flush on the application loop. An effect that raises is routed to the nearest error boundary like a render error, and a component that re-dirties itself for more than fifty passes raises RuntimeError("Too many re-renders") through the same path.

Equality accepts identity and scalar Boolean comparisons. Array-like comparisons that produce another array aren't coerced to a Boolean. Bound methods compare equal when they wrap the same function on the same object, so passing service.subscribe as a dependency or prop doesn't count as a change. Mutable objects must be replaced to signal a state or dependency change. Frozen dataclasses are a good choice for shared snapshots and Store values.

The renderer core

Below the reconciler, one C++ library does the platform-neutral work on every target: the renderer core in native/core/. iOS links it into the app binary, Android loads it as libpncore.so, tests and tooling load the pythonnative._core extension, and the browser preview runs it as WebAssembly. Python reaches it through a C ABI with ctypes.

Owner Responsibilities
Python (application thread) Components, hooks, reconciliation, commit operations, event handlers, effects, device module facades
Renderer core (C++) Contract tables and validation, surfaces and revisions, the shadow tree, Yoga style mapping and layout, view flattening, mount diffing, list datasets, animation graphs and drivers, gesture recognition (Android, browser, and tests), event identity stamping
Platform (UI thread) Widget creation and prop application, text drawing, text input, scroll physics, navigation containers, modal windows, accessibility, image decoding, the frame clock, iOS gesture recognizers, device modules, app lifecycle

A commit worker hands each commit to the core while the application loop remains available for input and asyncio tasks. The core validates the commit, lays it out off the UI thread, flattens layout-only views, and queues a binary mount transaction that the UI thread applies without waiting on any other thread. Queued commits prepare against the latest acknowledged revision. Refs, effects, and queued native events publish after acknowledgement. State writes during mounting coalesce into the following render. See The native bridge for the threads and the wire contract.

Native presentation

Swift and Kotlin component managers create widgets, apply props and frames, measure intrinsic controls on request, handle commands, and release resources. The platform UI thread owns every widget mutation and input callback. Because the core flattens views that only affect layout, a plain Row or Column wrapper usually doesn't become a UIView or FrameLayout. In the browser preview, the DOM managers in devserver/static/renderer.js play the same role: devserver/static/core/mount.js runs the WebAssembly core in the page and hands them each decoded transaction.

Navigation presents logical screen roots through a nested UIKit navigation controller or Android fragments. Pushing a screen doesn't create another Python application root. Providers and repositories above the navigator remain shared; covered screens retain their component state. Native lifecycle restoration uses the latest cached Python navigation state.

Virtualized lists keep row state in ordinary keyed components and mount a bounded window. Native collection and recycler views request rows asynchronously by key, index, and data revision. Measured heights replace estimates. Headers, footers, empty states, grouped grids, and sections use the same ownership model.

Contracts and tooling

The bridge protocol (version 7) validates commits in the core before mutation and acknowledges exact revisions. Events carry application and revision identities, which the core stamps. Controlled inputs additionally acknowledge native edit revisions to avoid overwriting newer typing. The core's animation graphs perform frame updates independently of Python callbacks.

The SDK compiles dataclass props and protocol methods into portable contracts. The core compiles the merged contract document into indexed tables at startup and validates every commit, command, and module call against them; typed Swift and Kotlin prop wrappers cache decoded fields for the managers. The core applies touched styles to Yoga directly and returns geometry only for observed refs and layout callbacks. ListData feeds revisioned keyed patches; ordinary sequences use a scanning adapter to that same protocol. Generated artifacts are checked in and tested for drift. Target-wheel plugin metadata is read as data, without importing a mobile binary on the development machine. Resources and registration code are staged into the native libraries.

pn deps --lock records target-specific wheel versions, URLs, and SHA-256 hashes. Release builds reject stale locks and missing targets; development builds warn and resolve the current requirements without the lock. Wheel tag compatibility and native SDK compatibility remain separate checks; a lock doesn't prove an extension can run on a device. Test every supported deployment target.

Fast Refresh preserves compatible component state. Changes to hook order or custom-hook signatures remount affected instances. Removing a class or changing its definition remounts the application to avoid retaining instances of the old definition; functions, TypedDicts, Protocols, and unchanged classes don't. Native contract changes require rebuilding the dev client. Development errors use a host-owned overlay with a traceback and reload/dismiss controls. It can report a poisoned renderer and reload a fresh surface without rendering an additional Python error tree.

Python phase timings, the core's commit and layout timings, and bridge work counters can be captured in Chrome trace format. See Profiling for collection and export instructions.

Runtime limits

The application host uses one surface. A failed bridge commit requires a complete surface reset and remount; see Commits. Reconciliation reduces work at dirty component roots but doesn't time-slice arbitrary Python render functions. Long computations delay Python callbacks and commits even while native scrolling and the core's animation drivers continue. Use cooperative async work, move blocking I/O off the application thread, and keep expensive computation out of render functions.

Shared layout rules don't imply identical fonts or control sizes. Test intrinsic measurement, accessibility, and performance on your deployment targets. Simulator and emulator tests exercise app behavior; they don't validate device signing, store submission, or performance on physical devices.

Source map

Area Location under src/pythonnative
Components and lifetimes component.py, element.py, hooks.py, runtime.py
Styles, themes, and stores style.py, theme.py, store.py
Props and dev-mode checks components/props.py, prop_checks.py, diagnostics.py
Rules-of-hooks linter lint.py
Reconciliation reconciler/, mutations.py, events.py
Bridge protocol bridge/, native_views/bridge_backend.py
Imperative handles and undo journal handles.py, journal.py
Renderer core (C++) and its Python binding native/core/ (Yoga in native/core/yoga/), bridge/core.py
Platform mounting layers native/ios/, native/android/
Navigation and lists navigation/, components/lists.py
Animation graphs animated.py, animation_graph.py
Contracts and plugins sdk/, project/plugins.py
Device modules native_modules/
Testing library and pytest plugin testing/
Build and dependency locks project/, cli/
Browser preview preview.py, bridge/web.py, devserver/static/ (the WebAssembly core in devserver/static/core/)

The Inbox example combines shared providers, variable-height rows, cancellable search, forms, persistence, native navigation, and a generated native extension.