Skip to content

Contributing

The canonical contributing guide lives at the repository root in CONTRIBUTING.md. The contents are included below so the docs site stays in sync.

Contributing to PythonNative

Thanks for your interest in contributing. This repository contains the PythonNative library, the pn CLI, Android/iOS project templates, example apps, and the documentation site. Contributions should keep the code reliable, cross-platform, and easy to use.

Quick start

Development uses Python ≥ 3.13 (the same versions the apps embed) and uv as the only prerequisite. uv sync creates and updates .venv itself, so there's no virtual environment to make and nothing to activate.

# install the project and its dev tooling into .venv, exactly as CI does
uv sync --locked --group dev

# one-shot: runs every CI check, in the same order
./scripts/check.sh

# run individual steps if you only want one
uv run pytest -q
uv run ruff check .
uv run black src examples tests

Dependencies are pinned in uv.lock, which is committed, so local runs and CI resolve identically. --locked fails rather than silently re-resolving if the lockfile has drifted from pyproject.toml. Upgrading a dependency is a deliberate uv lock --upgrade-package <name> in its own commit.

Common library and CLI entry points. uv run puts .venv/bin on the path for one command, so pn resolves without activating anything. Each block starts from the repository root.

# CLI help
uv run pn --help

# scaffold a sample app into ./my_app/, then work inside it
uv run pn init my_app
cd my_app
uv run pn run android

uv run looks upward for the nearest project, so it keeps using the repository's .venv from a subdirectory. The bundled example works the same way:

# run the Hello World example
cd examples/hello-world && uv run pn run android

Claiming an issue

To avoid duplicate work, claim an issue before you start on it:

  1. Check the issue's assignee and any linked pull requests. If either exists, the issue is taken.
  2. Comment on the issue to claim it and wait for a maintainer to assign it to you before opening a pull request.
  3. If you stop working on an assigned issue, leave a comment so it can be reassigned.

Unsolicited pull requests for issues that are already assigned or already have an open pull request will be closed as duplicates, even if the work is good.

Project layout (high‑level)

  • src/pythonnative/: installable library and CLI
  • Core Python modules, UI components, and utilities
  • bridge/: JSON codec and per-platform transports into the native rendering core
  • cli/: pn command
  • project/: config loading, template configuration, native plugin staging, and the builder behind pn
  • native/: native libraries and their tests, bundled with the Python package
    • ios/: PythonNativeKit Swift package and its XCTest target
    • android/: Android rendering library and its JUnit target
    • yoga/: vendored Yoga source, native build definitions, and the host Python binding
  • sdk/: native contract definitions, code generation, and generator templates
  • devserver/static/: browser preview assets, including the vendored Yoga JavaScript/WebAssembly distribution
  • templates/: Android/iOS app shells; the builder stages native libraries into generated app projects
  • tests/: unit tests for the library, plus the Maestro E2E suite
  • e2e/: the comprehensive E2E suite (see E2E tests below and tests/e2e/AGENTS.md)
  • examples/: runnable example apps
  • hello-world/: minimal marketing demo
  • e2e-suite/: comprehensive feature catalog that drives the Maestro E2E suite
  • inbox/: offline reference app with a custom native extension
  • scripts/: helper scripts (check.sh, run-e2e.sh, check-e2e-coverage.py)
  • README.md, pyproject.toml: repo docs and packaging

Coding guidelines

  • Style: Black; lint: Ruff; typing where useful. Keep APIs stable.
  • Prefer explicit, descriptive names; keep platform abstractions clean.
  • Python never imports platform code. Everything that touches UIView or android.view.View lives in Swift (PythonNativeKit) or Kotlin (the pythonnative Gradle module) and is reached through pythonnative.bridge; see docs/concepts/bridge.md for the protocol. Bump PROTOCOL_VERSION on both sides when the wire format changes.
  • Add/extend tests under tests/ for new behavior. Native changes get XCTest / JUnit coverage next to the code they touch.
  • Design large changes in an RFC first. Anything that adds or removes public API, changes the Python-to-native wire contract, changes a convention apps depend on, or spans Python, Swift, Kotlin, and the browser preview gets a document under rfcs/ (see rfcs/README.md). The RFC lands with the implementation and records what shipped, including removals.
  • Commit source, reviewed generated contracts, dependency locks, and vendored dependencies as described in Generated source and vendored dependencies. Don't commit local build outputs or caches.
  • Docstrings: Google style throughout. Ruff is configured with the Google convention (pydocstyle.convention = "google") and enforces the D rule set on src/pythonnative/. See the Documentation style guide for examples and Markdown/grammar conventions.

Common commands:

./scripts/check.sh              # run all CI checks (mirrors ci.yml)
uv sync --locked --group dev    # install/refresh .venv from the lockfile
uv run pytest -q                # run tests
uv run ruff check .             # lint
uv run black src examples tests # format
uv run --group docs mkdocs serve # preview the docs site locally

# native rendering core (macOS with Xcode for the Swift package; JDK 17 for Gradle)
(cd src/pythonnative/native/ios && xcodebuild test -project Tests/NativeTests.xcodeproj -scheme NativeTests -destination 'platform=iOS Simulator,name=iPhone 15 Pro')
(cd src/pythonnative/native/android && ../../templates/android_template/gradlew testDebugUnitTest)

Generated source and vendored dependencies

Some generated files and third-party distribution files are intentional parts of the source tree. Keep these in version control:

  • Native contracts: PNContracts, NativeProps, and NativeModules in src/pythonnative/native/ios/Sources/PythonNativeKit/Generated/ and src/pythonnative/native/android/src/main/java/com/pythonnative/generated/. These support standalone native builds. Regenerate them from the Python definitions and generator templates instead of editing them by hand.
  • Example extension schemas, such as examples/inbox/native/schema.json. Commit schema updates alongside their Python definitions and native implementations.
  • Dependency locks, including uv.lock and example apps' pn.lock files.
  • Vendored Yoga source and browser distribution files under src/pythonnative/native/yoga/ and src/pythonnative/devserver/static/yoga/. The browser distribution includes its JavaScript/WebAssembly runtime, type declarations, and source maps. Preserve upstream licenses and document the version, source, and any local modifications when updating vendored code.

After changing built-in contracts or their generators, regenerate the checked-in native files from the repository root:

uv run pn codegen --output build/contracts
cp build/contracts/{PNContracts,NativeProps,NativeModules}.swift \
  src/pythonnative/native/ios/Sources/PythonNativeKit/Generated/
cp build/contracts/{PNContracts,NativeProps,NativeModules}.kt \
  src/pythonnative/native/android/src/main/java/com/pythonnative/generated/
uv run pytest tests/test_codegen.py

The test checks that the committed native contracts match generation in a clean Python interpreter. The other files under build/contracts/ are local outputs; don't add the whole directory to the commit.

Exclude staged app projects, compiled apps and libraries, package distributions, test reports, local storage, credentials, and tool caches. Examples include build/, dist/, .build/, .gradle/, .cxx/, .swiftpm/, and __pycache__/. The vendored browser runtime above is an explicit exception to excluding compiled dependencies; downloaded Python runtimes and locally compiled binaries aren't.

Conventional Commits

This project uses Conventional Commits. Use the form:

<type>(<scope>): <subject>

[optional body]

[optional footer(s)]

Commit message character set

  • Encoding: UTF‑8 is allowed and preferred across subjects and bodies.
  • Keep the subject ≤ 72 chars; avoid emoji.

Accepted types (standard):

  • build: build system or external dependencies (e.g., requirements, packaging)
  • chore: maintenance (no library behavior change)
  • ci: continuous integration configuration (workflows, pipelines)
  • docs: documentation only
  • feat: user‑facing feature or capability
  • fix: bug fix
  • perf: performance improvements
  • refactor: code change that neither fixes a bug nor adds a feature
  • revert: revert of a previous commit
  • style: formatting/whitespace (no code behavior)
  • test: add/adjust tests only

Recommended scopes (choose the smallest, most accurate unit; prefer module/directory names):

  • Module/directory scopes:
  • alerts: imperative Alert/Picker helpers (alerts.py)
  • animated: Animated namespace and animation primitives (animated.py)
  • bootstrap: on-device runtime start-up called by the templates (bootstrap.py)
  • bridge: wire codec, transports, handshake, and native-to-Python callback (bridge/)
  • cli: CLI tool and pn command (src/pythonnative/cli/)
  • component: the @component decorator, Component, and memo (component.py)
  • components: declarative element-creating functions (components/)
  • element: Element descriptor class (element.py)
  • events: tag-based event routing between native views and Python callbacks (events.py)
  • gestures: gesture descriptors and the pure-Python recognition arbiter (gestures.py)
  • hooks: hooks and contexts (hooks.py)
  • hosts: screen hosts, lifecycle forwarding, and render scheduling (hosts/)
  • devserver: dev server, file watcher, and WebSocket implementation (devserver/)
  • devclient: on-device dev client that syncs sources and Fast Refreshes (devclient.py)
  • hot_reload: module reloader and Fast Refresh (hot_reload.py)
  • layout: Yoga layout integration and the host binding (layout.py, native/yoga/)
  • mutations: batched mutation ops between reconciler and native backends (mutations.py)
  • native_modules: native module registry, Python facades, and Python fallbacks (native_modules/)
  • native_views: view registry protocol and bridge backend (native_views/)
  • navigation: navigation state, container, navigators, hooks, and linking (navigation/)
  • net: awaitable HTTP client (net.py)
  • package: src/pythonnative/__init__.py exports and package boundary
  • platform: Platform.OS/Platform.select and version detection (platform.py)
  • platform_metrics: platform-reported metrics like safe-area insets and bar heights (platform_metrics.py)
  • preview: pn start / pn preview session and the browser preview page (preview.py, devserver/static/)
  • project: pythonnative.toml config, template configurators, plugin staging, and the builder (project/)
  • reconciler: virtual view tree diffing, boundaries, and the layout pass (reconciler/)
  • runtime: framework-wide asyncio loop and thread-safe future helpers (runtime.py)
  • sdk: public extension SDK for custom native components (sdk/)
  • storage: AsyncStorage key/value persistence and use_persisted_state (storage.py)
  • scheduler: render batching and transition queues (scheduler.py)
  • style: StyleSheet and theming (style.py)
  • testing: public test utilities (testing/)
  • utils: shared utilities (utils.py)

  • Other scopes:

  • deps: dependency updates and version pins
  • examples: example apps under examples/
  • mkdocs: documentation site (MkDocs/Material) configuration and content under docs/
  • pyproject: pyproject.toml packaging/build metadata
  • repo: repository metadata and top‑level files (README.md, CONTRIBUTING.md, .gitignore, licenses)
  • scripts: developer scripts under scripts/ (e.g., check.sh)
  • templates: Android/iOS project templates under src/pythonnative/templates/
  • kit: the Swift rendering core (native/ios/)
  • runtime-android: the Kotlin rendering core (native/android/)
  • tests: unit/integration/E2E tests under tests/
  • workflows: CI pipelines under .github/workflows/

Note: Avoid redundant type==scope pairs (e.g., docs(docs)). Prefer a module scope (e.g., docs(core)) or docs(repo) for top‑level updates.

Examples:

build(deps): refresh pinned versions
chore(repo): add contributing guidelines
ci(workflows): add publish job
docs(reconciler): clarify diffing algorithm
feat(components): add Slider element
fix(cli): handle missing Android SDK gracefully
perf(reconciler): reduce allocations in list diffing
refactor(utils): extract path helpers
test: cover iOS template copy flow

Examples (no scope):

build: update packaging metadata
chore: update .gitignore patterns
docs: add project overview

Breaking changes:

  • Use ! after the type/scope or a BREAKING CHANGE: footer.
feat(hosts)!: rename create_page to create_screen

BREAKING CHANGE: API renamed; update app code and templates.

Multiple scopes (optional)

  • Comma‑separate scopes without spaces: type(scope1,scope2): ...
  • Prefer a single scope when possible; use multiple only when the change genuinely spans tightly related areas.

Scope ordering (house style):

  • Put the most impacted scope first (e.g., repo), then any secondary scopes.
  • For extra consistency, alphabetize the remaining scopes after the primary.
  • Keep it to 1–3 scopes max.

Example:

feat(templates,cli): add ios template and wire pn init

Pull requests and squash merges

  • PR title: use Conventional Commit format.
  • Example: feat(cli): add init subcommand
  • Imperative mood; no trailing period; ≤ 72 chars; ! for breaking changes.
  • PR description: include brief sections: What, Why, How (brief), Testing, Risks/Impact, Docs/Follow‑ups.
  • Link issues with keywords (e.g., Closes #123).
  • Merging: prefer "Squash and merge" with "Pull request title and description".
  • Keep PRs focused; avoid unrelated changes in the same PR.

Recommended PR template:

What
- Short summary of the change

Why
- Motivation/user value

How (brief)
- Key implementation notes or decisions

Testing
- Local/CI coverage; links to tests if relevant

Risks/Impact
- Compat, rollout, perf, security; mitigations

Docs/Follow-ups
- Docs updated or TODO next steps

Closes #123
BREAKING CHANGE: <details if any>
Co-authored-by: Name <email>

Pull request checklist

  • PR title: Conventional Commits format (CI-enforced by pr-lint.yml).
  • Tests: added/updated; uv run pytest passes.
  • Lint/format: uv run ruff check . and uv run black --check src examples tests pass.
  • Docs: update README.md if behavior changes.
  • Templates: update src/pythonnative/templates/ if generator output changes.
  • Generated contracts and example schemas are regenerated and reviewed when changed; vendored dependencies retain their licenses and provenance.
  • No local build outputs, caches, credentials, or machine-specific files are committed.

Versioning and releases

  • The version is tracked in pyproject.toml (project.version) and mirrored in src/pythonnative/__init__.py as __version__. Both files are updated automatically by python-semantic-release.
  • Automated release pipeline (on every merge to main):
  • python-semantic-release scans Conventional Commits, updates the version files and uv.lock, generates CHANGELOG.md, and pushes the release commit and tag.
  • A published GitHub release is created with generated release notes.
  • The shared Distributions workflow builds a source archive from that exact commit, then uses cibuildwheel to build all wheels from the archive.
  • Each wheel is repaired where needed, installed in an isolated environment, and tested with the Yoga layout suite and CLI. The complete artifact set must also pass platform, version, resource, and metadata checks.
  • Validated distributions are attached to the GitHub release before PyPI uploads begin. PyPI uses Trusted Publishing; no API token is needed.
  • The same distribution builds and checks run on PRs. The wheel matrix covers CPython 3.13 and 3.14 on Linux x86-64 and ARM64 (glibc 2.28 or newer), macOS Intel and Apple Silicon (macOS 11 or newer), and Windows x64. These are development-host wheels; mobile apps compile the bundled Yoga source in their native builds.
  • Build policy lives in scripts/cibuildwheel.toml, separately from the tagged package source, so fixed tooling can rebuild an existing release without changing its code or version.
  • Commit types that trigger a release: feat (minor), fix and perf (patch), and BREAKING CHANGE (major, or minor before 1.0). Other types, including build and ci, don't trigger a release on their own. Use a build or ci title for a publishing-only repair that should recover the existing version.
  • Tag format: v-prefixed (for example, v0.40.0). Manual version bumps aren't needed.

Recovering a failed publication

Version creation and package publication are separate jobs. A GitHub release can exist even when its PyPI upload failed. Rerunning version creation won't create another release for the same commits.

To resume publication, select Actions → Release → Run workflow, choose main, and enter the existing tag in the recovery field. If the failure requires a workflow repair, merge that repair first. For example, with the GitHub CLI:

gh workflow run release.yml --ref main -f tag=v0.40.0

Recovery verifies that the tag belongs to main and matches the package version, then runs the same build and validation jobs. It doesn't bump the version, rewrite the tag, or change the tagged source. Existing distribution assets are reused byte for byte; missing assets are uploaded before publishing to PyPI. Files already uploaded to PyPI are skipped, so a partial upload can resume. Release runs are serialized to prevent competing uploads.

For a build-only rehearsal, run Distributions with a source-ref of the tag or commit to check. This runs the full matrix without publishing anything:

gh workflow run wheels.yml --ref main -f source-ref=v0.40.0

Branch naming (suggested)

  • Use lowercase kebab‑case; concise (≤ 40 chars).
  • Branch prefixes match Conventional Commit types:
  • feat/<scope>-<short-desc>
  • fix/<issue-or-bug>-<short-desc>
  • chore/<short-desc>
  • docs/<short-desc>
  • ci/<short-desc>
  • refactor/<scope>-<short-desc>
  • test/<short-desc>
  • perf/<short-desc>
  • build/<short-desc>

Examples:

feat/cli-init
fix/core-threading-deadlock-123
docs/contributing
ci/publish-pypi
build/lock-versions
refactor/utils-paths
test/templates-android
fix/cli-regression

E2E tests (Maestro)

End-to-end tests use Maestro to drive the dedicated examples/e2e-suite app on real emulators and simulators. That app contains one screen per public symbol in pythonnative.__all__; every flow under tests/e2e/flows/<category>/ exercises one symbol.

The dedicated examples/hello-world app is left in place as a small marketing demo; it's not the E2E target.

# Install Maestro (one-time)
curl -Ls "https://get.maestro.mobile.dev" | bash

# For iOS, also install idb-companion
brew tap facebook/fb && brew install idb-companion

Build and run everything via the convenience script:

# Android (emulator must be running)
uv run ./scripts/run-e2e.sh android

# iOS (simulator must be running)
uv run ./scripts/run-e2e.sh ios

For tight iteration, run a single category instead of the full pass:

uv run ./scripts/run-e2e.sh android hooks
uv run ./scripts/run-e2e.sh ios components

Available categories: components, hooks, navigation, layout, styling, animations, misc.

A coverage checker, scripts/check-e2e-coverage.py, gates CI: every name in pythonnative.__all__ must be covered by a demo + flow, or listed in INTENTIONAL_EXEMPTIONS with a justification.

When you add a new public symbol you must also:

  1. Add a demo screen under examples/e2e-suite/app/screens/<category>/.
  2. Append a DemoEntry in examples/e2e-suite/app/registry.py.
  3. Add a Maestro flow at tests/e2e/flows/<category>/<name>.yaml.
  4. Append the flow to the top-level tests/e2e/android.yaml, tests/e2e/ios.yaml, and the matching tests/e2e/suites/<category>.yaml.
  5. Confirm uv run python scripts/check-e2e-coverage.py exits 0.

tests/e2e/AGENTS.md is the deeper reference (label conventions, failure triage, naming rules); AI agents should read it before touching the suite. The e2e.yml workflow runs the suite automatically on pushes to main and PRs.

CI

  • CI (ci.yml): runs formatter, linter, type checker, and tests on every push and PR.
  • E2E (e2e.yml): builds examples/e2e-suite on Android (Linux emulator) and iOS (macOS simulator), then runs Maestro flows by category. Triggers on pushes to main, PRs, and manual dispatch.
  • Packages (packages.yml): resolves the PyPI compatibility matrix in tests/packages/matrix.toml against the live indexes with scripts/package-matrix.py --check, weekly and on changes to the resolver or manifest, and uploads the rendered Markdown table for docs/guides/pypi-packages.md.
  • PR Lint (pr-lint.yml): validates the PR title against Conventional Commits format (protects squash merges) and checks individual commit messages via commitlint (protects rebase merges). Recommended: add the PR title job as a required status check in branch-protection settings.
  • Release (release.yml): runs on merge to main; computes version, generates changelog, tags, creates GitHub Release, and (when DRAFT_RELEASE is "false") publishes to PyPI.
  • Docs (docs.yml): builds the MkDocs site in strict mode on every push and pull request, and deploys to GitHub Pages on push to main.

Security and provenance

  • Avoid bundling secrets or credentials in templates or code.
  • Prefer runtime configuration via environment variables in CI.

License

By contributing, you agree that your contributions are licensed under the repository's MIT License.