Write typed Python for servers, browsers, and Android—without shipping a Python runtime to JavaScript targets.
PyJS statically analyzes reachable Python and emits readable, tree-shaken JavaScript. The same component can render real HTML on the server, hydrate as a native custom element in the browser, and share application logic with a direct-JVM Android app.
PyJS is early-release software: complete applications and tests work today, while APIs can still change between releases.
Browser JavaScript and direct-JVM Android are the application-facing targets. The JVM, LLVM, and WebAssembly backends remain experimental; their APIs and supported subsets may change rapidly.
python -m pip install "pyjs[examples]"
python -m pyjs_examples.calculatorOpen the clickable local URL printed by the server. The Calculator is one Python application targeting the Web, an installable PWA, and Android. Try the source, then inspect its browser and Android tests beside it.
PyJS keeps the generated result recognizable. Given:
from pyjs import js
@js
def total(prices: list[int]) -> int:
return sum(prices)
print(total([12, 30]))pyjs --src -c '...' emits:
export function total(prices) {
return (prices).reduce((total,value)=>total+value,0);
}
export function main() {
console.log(total([12, 30]));
}There is no bundled Python interpreter, virtual DOM, or JavaScript project to maintain.
Install PyJS and create a project:
python -m pip install pyjs
pyjs create hello_pyjs
cd hello_pyjs
python -m pip install -e ".[test]"
python -m hello_pyjsOpen http://127.0.0.1:8000/. The generated project has no Node.js configuration or JavaScript source: it contains a custom element, server entry point, Python tests, and a browser test.
Run its test suite on either target:
ruff check .
python -m pytest -q
python -m pytest -q --pyjs-target=web --pyjs-browser=managedTo develop PyJS itself from a source checkout, see CONTRIBUTING.md.
The LLVM backend compiles a supported subset of typed Python to machine code, without embedding CPython. During development, try it from a source checkout with Python 3.14, uv, and a native Clang toolchain installed:
git clone https://github.com/pyjs/pyjs.git
cd pyjs
uv syncSave this as primes.py in the checkout. It counts primes with a sieve,
exercising list allocation, indexing, loops, branches and a reduction:
def count_primes(limit: int) -> int:
prime = [True] * (limit + 1)
prime[0] = prime[1] = False
factor = 2
while factor * factor <= limit:
if prime[factor]:
for multiple in range(factor * factor, limit + 1, factor):
prime[multiple] = False
factor += 1
return sum(prime)
def main() -> None:
print(count_primes(2_000_000))
if __name__ == "__main__":
main()Run the same file with either interpreter or compiler—no decorators or source
strings are needed. Both print 148933:
uv run python primes.py
uv run pyjs --target native primes.py
uv run pyjs --target native --perf primes.pyThe last command compiles once, checks matching output and successful exits on
every run, then reports paired process timings. A Linux x64 run with CPython
3.14.4 and Clang 21 at -O2 produced these timings (shown with the current
compiler label):
Compilation: 1.818 s (21.1.8, O2)
Output matches: yes
148933
Median of 7 runs after 2 warmups (process startup included):
CPython 3.14.4: 183.171 ms
Native: 13.317 ms
Speedup: 13.75x (compilation excluded)
These are measurements, not a universal speed guarantee. Compilation costs
more than a single interpreted run of this example; native execution pays off
when you reuse the binary or run a larger workload. --perf alternates which
program runs first, includes process startup and captured output for both,
and excludes compilation and the PyJS CLI's startup from execution timings.
Use --samples N, --warmups N or --json to inspect your own results.
On Windows a small CPython launcher configures UTF-8 output without newline
translation to match native output; its startup is included. Captured output
is compared byte-for-byte, not normalized after execution.
Keep an executable to run repeatedly without recompiling:
uv run pyjs build --target native primes.py -o primes
./primesOn Windows the output is primes.exe; run .\primes.exe. Native execution also
accepts -m package.module, -c 'print(42)' and stdin (-). Native builds
accept a file or module, optionally followed by :entry_function. Use
pyjs --target native --src primes.py to inspect the LLVM IR.
Default build output uses the source filename's stem (__main__ for a package
entry); use -o build/app to choose a package executable's name without colliding
with the package's source directory.
Clang also needs the host's linker and C runtime
development libraries: Xcode Command Line Tools on macOS, or MSVC Build Tools,
the Windows SDK and Clang's compiler-rt on Windows. Use matching 64-bit Python
and compiler architectures. Set PYJS_CLANG to a compiler path if it is not on PATH.
This compiles for the current host, not an arbitrary cross-compilation
target. CI compiles and executes on Linux, macOS and Windows, each on x64 and
arm64, including loading native libraries into Python. The executable entry
point is a zero-argument main() returning None (success),
str (written to stdout) or int (used as the process exit status). Simple
top-level statements can be wrapped automatically. For scripts with helpers or
a __name__ guard, use the guarded main() pattern above; pass configuration
into helpers rather than reading unannotated module globals. This avoids shared
analysis limitations and gives the same behavior under CPython and native execution;
CPython normally ignores a function's returned value. Imports, decorators and
module initialization execute in Python during analysis, so compile only code
you trust. Program command-line arguments are not yet supported. Arbitrary
Python packages and dynamic Python features are not automatically supported.
Initialization output is reported as compilation diagnostics on stderr, or
under compilation with --json, rather than mixed into the native result or
generated LLVM source.
Embedded hosts can select source implementations of imported interfaces with
library_mappings= on transpile_llvm, transpile_llvm_module,
LLVM.run_source, or LLVM.compile_library (and their comparison helpers).
Subclass pyjs.native.analysis.NativeLibraryMappings, override resolve(value)
to return the host implementation for a recognized Python class or function,
and delegate unhandled values to super().resolve(value). Pass the resolver
instance explicitly for each compilation; it applies during analysis and
emission without modifying the default native resolver or Python imports.
The native semantic builtins and ABI remain unchanged. These mappings select
implementations; they do not provide a browser runtime, event loop, or additional
language support. Host implementations must use the supported native subset.
The native backend supports async def and nested await through managed
coroutine frames. Calling an async function creates a coroutine without running
its body. A synchronous driver can obtain coroutine.__await__() and advance
that iterator with next, send, throw, and close; completion arrives as
StopIteration.value. Custom awaitables can define __await__ returning a
typed Generator and use their concrete class as the annotation. Native
Awaitable[T] storage currently represents coroutine results only; inheriting
from Awaitable is rejected rather than reinterpreting an application object
as a coroutine frame. Async callbacks and methods use the same call machinery.
The compiler does not supply an event loop or asyncio implementation. Native
executable and library entry points remain synchronous, and async generators
are not implemented. A host scheduler must own pending coroutine iterators,
resume them when operations finish, and close them before disposing its heap.
- Python stays visible. Generated JavaScript is readable and maps back to the original Python in browser DevTools.
- HTML is the handoff. Components render on the server, retain small typed state in the DOM, and hydrate the elements already on screen.
- The browser is the framework. Custom elements, attributes, events, and Web APIs remain direct platform primitives rather than a virtual DOM.
- Types remove runtime weight. Static analysis specializes Python semantics and tree-shakes code from each entry point.
- Escape hatches stay open. Typed browser APIs and raw transports remain available beneath the UI and full-stack conveniences.
Coding agents can run pyjs skill for version-matched guidance and
pyjs template to discover complete executable examples. The
installable PyJS skill is included in the package
and public repository.
The separately installed examples progress from a one-file counter to composed full-stack applications:
| Example | Purpose | Principal coverage |
|---|---|---|
| Counter | Smallest app | DOM, events, tests |
| Calculator | Universal app | Web, PWA, Android, SSR |
| Components | UI reference | Widgets, forms, routing |
| Notes | Personal app | Android, drag/drop, restore |
| Chat | Real-time starter | WebSockets, multi-window |
| Store | Business app | Django, JSON:API, streams, themes |
| Space Invaders | GPU game | WebGPU, render tests |
Inspect or copy any of them:
python -m pip install "pyjs[examples]"
python -m pyjs_examples.calculator
pyjs template
pyjs template pyjs_examples.calculator
pyjs template pyjs_examples.calculator --src
pyjs create my_calculator --template pyjs_examples.calculator
pyjs create counter_demo --template pyjs_examples.counter
pyjs create my_counter --single-file --template pyjs_examples.counter
pyjs create components_demo --template pyjs_examples.componentsThe template catalog contains examples that can be copied safely out of their installed package. Store is a comprehensive runnable application instead of a scaffold because its Django package, fixture data, and optional server dependencies belong together. Run it directly with:
python -m pip install "pyjs-examples[store]"
python -m pyjs_examples.storepyjs create NAME creates a project in NAME/, with application code in
NAME/NAME/NAME.py and tests in NAME/tests/. Pass a destination as the
second positional argument to populate another directory, such as
pyjs create example .. Add --src-layout to put the package under src/.
After installation, python -m NAME starts either package layout through its
ordinary Python entry point. pyjs serve is also available as a convenience
and uses the application recorded in pyproject.toml.
pyjs serve --open /some/path/ opens that local URL after the server starts;
--open alone opens /. Use --host and --port to choose the listener
(--port 0 selects an available port). Explicit module:create_app targets
can return a pyjs.server.dev.Application for live Python routes.
--single-file explicitly requests one standalone Python file and requires a
file template when combined with --template. The pyjs-examples
distribution registers both file and directory templates and contains the
source copied by pyjs create; templates are not embedded in the scaffolding
implementation.
For Node-compatible code, PyJS can compile and run a file, module, or command string directly with its managed Node runtime:
pyjs app.py
pyjs -m package.module
pyjs -c 'print(6 * 7)'
pyjs -v -c 'print(6 * 7)' # generated JavaScript followed by Node output
pyjs --src app.py # generated source only
pyjs --src-all app.py # entry source and tree-shaken dependencies
pyjs --perf -c 'print(sum(range(10)))'Use --json for structured results or --perf to compare Python and generated
JavaScript timing. DOM, WebGPU, and other browser APIs still run through an
application or browser test.
Node provisioning uses sdkmgr, which reuses
mise or nvm and installs the requested version only when necessary. Explicit
Node(executable=...) and PYJS_NODE selections still take precedence and
require Node 20 or newer. Previous PyJS-managed Node caches are left untouched;
new installations belong to the selected manager. Configure mirrors through
that manager, not the retired PYJS_NODE_MIRROR setting. Constructing a Node
does not install anything; ensure() provisions it before execution. Path
properties only discover existing installations and report a missing runtime.
The optional root argument now controls sdkmgr's manager-bootstrap storage,
not the Node installation directory.
The application server builds and serves browser assets automatically. To write the generated JavaScript, CSS, and HTML explicitly:
pyjs build hello_pyjs.hello_pyjs:mainUse --only-js when only the browser bundle is needed.
Package a Python UI as a desktop application:
pyjs build my_app:main --target electron -o dist/my-appThe build includes the generated HTML, CSS, JavaScript and Python source maps,
plus the stock Electron runtime. Python and an HTTP server are not needed to run
it. Run dist/my-app/electron on Linux, dist/my-app/electron.exe on Windows,
or open dist/my-app/Electron.app on macOS. These are application directories,
not signed installers; distribution, signing and updates remain separate steps.
The first build provisions Electron 45.0.0 through its exact npm package using
PyJS's managed Node tooling. The build runs Electron's own installer, which
verifies its published checksums. This requires network access. Use
--electron-runtime PATH to reuse an existing host-platform Electron
distribution offline. The output must not already exist. Use a distinct output
directory for each concurrent build; concurrent modification of the destination
is unsupported. Runtime links used for application resources must resolve inside
the copied distribution, and the runtime must not contain a pre-existing app.
Linux still requires a working Chromium sandbox. Systems that restrict unprivileged user namespaces may require an administrator-configured AppArmor policy or a correctly installed root-owned setuid sandbox helper. Packaging does not configure the operating system or disable Electron's sandbox; handle this in your installation process.
The default window uses a sandboxed renderer with context isolation and no
Node globals. UI code, state and DOM access stay in the renderer. Existing
HTTP backend routes are not automatically translated into desktop services.
Pages load from file://, which can read user-accessible local files despite
the renderer sandbox. There is no default Content Security Policy restricting
outbound connections. These defaults are for trusted application UI, not a
boundary for running untrusted content. For Node filesystem operations (such as
writes) or other OS services, pass --electron-host host-directory.
A Python host package contains main.py with an async main(context) entry
and, optionally, preload.py with a zero-argument main(). The build compiles
both entries and their reachable dependencies into self-contained CommonJS
files with Python source maps. Nothing is compiled at application launch.
For example, desktop_host/main.py:
from pyjs import js
from pyjs.electron import HostContext, WindowOptions
@js
async def main(context: HostContext):
window = context.createWindow(WindowOptions("My application"))
await window.loadFile(context.renderer)pyjs.electron exposes typed window, IPC, context-bridge and debugger operations;
pyjs.node exposes typed filesystem, process and timer operations. These are
JavaScript-host APIs, not CPython implementations of Node. A sandboxed preload
can use Electron's bridge/IPC APIs but cannot use Node filesystem or subprocess
bindings. Keep it limited to a small, fixed capability surface. Its Python
imports are bundled; it does not load other JavaScript files at runtime.
The host package must be importable and have a unique module name in a build
process. Existing JavaScript hosts with main.cjs exporting async start(context)
remain supported; their files are copied unchanged. If both main files exist,
Python takes precedence.
Use pyjs.electron.window.on_close(window, callback) to handle a native
close request. Calling event.preventDefault() synchronously keeps the
window open while the application saves state. window.close() requests a
normal close; window.destroy() bypasses the close event.
A host can notify its renderer with window.webContents.send(channel, text).
In the preload, pyjs.electron.listen(channel, callback) delivers the text
and returns an unsubscribe function. Removing a subscription does not remove
other listeners. Expose fixed-channel callbacks through expose; the listener
adapter deliberately does not forward the Electron event into the renderer.
Build standalone Node programs from an importable Python module with:
pyjs build my_service:main --target node -o dist/service.cjs
node --enable-source-maps dist/service.cjsThe generated module exports main without calling it when imported. Direct
execution awaits it and reports failures with a nonzero exit status. Source maps
point back to Python. Use pyjs.node.json to exchange ordinary Python dictionaries
with JSON host APIs. Other Python standard-library modules are not automatically
translated into Node APIs.
For bounded host reads, pyjs.node.files provides open_readonly, fstat
and read_into with caller-owned byte buffers and descriptors. It also exposes
lstat, fsync and no-replace hard-link publication through link. Close
descriptors in finally with pyjs.node.close_descriptor. Requested
nofollow/nonblocking flags fail explicitly when the platform lacks them;
no-follow protects only the final path component. These low-level operations
do not provide path containment, a snapshot, a lock or a filesystem transaction.
FileInfo uses Node's Number-valued metadata. Compare dev and ino as file
identity only after checking Number.isSafeInteger for both values; large
filesystem identifiers can lose precision, especially on Windows.
open_directory returns an async Directory handle for incremental read()
calls (ending with None); call close() in finally. Unlike readdir, it
does not first materialize every entry in a large directory. Keep backup,
recovery and directory validation in the application host.
Python-authored Node tests use @js and pytest.mark.pyjs_node and run with
pytest --pyjs-target=node. Each zero-argument, module-level test is compiled
and executed in an isolated Node process, including async tests. Fixtures and
parametrized arguments are not transported into Node; host-side autouse fixtures
may prepare external resources or environment variables. Ordinary pytest runs
skip Node-only tests. An entry that exits with an unfinished await fails rather
than silently passing. Tests time out after 60 seconds; timeout and interruption
cleanup also terminates child processes still descended from the test, including
detached children. Tests must not daemonize processes that outlive their parents.
Generated bundles belong in build/test output, not Git.
A custom host owns its preload, IPC handlers and window creation, including
validating service inputs and keeping any Node dependencies self-contained.
createWindow applies the default window restrictions; callers can customize
options explicitly. Keep IPC coarse-grained: send a file or a batch of results,
while updating the UI locally. The host runs with Node privileges; only package
trusted application code. Source maps embed the Python sources for debugging.
An application entry point returns a server-rendered DOM tree. Classes
decorated with @js are available to the browser compiler, and reachable
methods are included in the generated bundle. CustomElement subclasses
render on the server and hydrate as browser-native custom elements. ref()
reconnects Python attributes to existing server-rendered nodes rather than
recreating them.
Module values cross the same explicit compilation boundary through a JS
annotation. Combine it with Final for constants, such as
heartbeat_interval: Final[JS] = 5_000; unmarked module values remain
server-only and cannot be included by an accidental browser-side reference.
Values that must survive rendering and hydration can live on the custom
element as DOM attributes. The state() descriptor and @effect(...) provide
a small typed reactive layer for updating the DOM imperatively. Server-only
handlers and browser methods can coexist in the same module.
PyJS starts at the selected entry point and includes reachable browser code.
In this example, sleep() and run() are reachable; eat() and bark() are
omitted:
from pyjs import js
@js
class Animal:
def sleep(self):
pass
def eat(self):
pass
@js
class Dog(Animal):
def run(self):
pass
def bark(self):
pass
def main():
dog = Dog()
dog.sleep()
dog.run()Ordinary event handlers and called methods do not need include=True. That
option remains an escape hatch for unusual methods invoked dynamically by
JavaScript.
The analyzer infers types from literals, constructors, calls, and control flow. Empty collections and public boundaries generally need annotations because their element or return type cannot be recovered from a value:
from pyjs import js
@js
class Animal:
def __init__(self, name: str):
self.name = name
def get_name(self) -> str:
return self.name
def main():
values = {"one": 1, "two": 2}
animal = Animal("Hazel")
print(values["one"])
print(animal.get_name())Type errors are reported during analysis instead of being deferred to generated JavaScript. Diagnostics are improving and do not yet cover every invalid program equally well.
Union types narrow through isinstance(), branches, assertions, and null
checks:
from pyjs import js
@js
def length(items: dict[str, int] | list[int] | str) -> int:
if isinstance(items, dict):
return len(items)
if isinstance(items, list):
return len(items)
assert isinstance(items, str)
return len(items)
def main():
length(["one"])
length({"one": 1, "two": 2})
length("four")is None and is not None compile to nullish JavaScript comparisons, covering
both JavaScript null and undefined, while identity comparisons between
ordinary objects retain identity semantics.
Built-in operators use their normal JavaScript equivalents for compatible primitive values and dispatch to Python special methods for user-defined classes:
from pyjs import js
@js
class ListAdder:
def __init__(self):
self.values: list[str] = []
def __add__(self, other: str):
self.values.append(other)
return self
def __str__(self) -> str:
return "|".join(self.values)
def main():
values = ListAdder()
values += "a"
values += "b"
print(values)The analyzer preserves concrete element and key/value types for built-in
collections such as list[T], dict[K, V], and tuple[...]. User-defined
generic specialization exists in the analyzer but remains experimental and is
not yet a stable application-facing feature.
Typed tuples and lists can be unpacked into local names. Tuple and literal-list arity is checked during compilation; dynamic lists retain Python's runtime arity errors. Nested, starred, and chained unpacking targets are not yet supported.
Async functions and await lower to JavaScript promises. Typed browser APIs
such as fetch(), WebSockets, and WebGPU use the same syntax as asynchronous
Python code. Basic raising and exception handling are supported, but exception
semantics are not yet a complete match for CPython.
Use pyjs.web.async_lock.AsyncLock to serialize overlapping browser callbacks
that read state, await a request, and then update that state. Acquire before
reading the shared state, and release in finally so errors cannot strand the
queue:
await lock.acquire()
try:
await save_changes()
finally:
lock.release()Acquisition is FIFO and non-reentrant. The browser implementation does not
provide task cancellation or cross-thread synchronization; host-side tests use
an asyncio.Lock with the same acquire/release interface. Create host locks
per event loop; do not reuse a contended lock across separate asyncio.run()
calls.
Module and class bodies execute in Python before analysis, so ordinary Python
metaprogramming can construct the definitions that PyJS later analyzes. The
@js decorator also supports client replacements and inline call generation
for typed browser API bindings. Compilation extensions can rewrite function
ASTs and inject imports before analysis; the browser-testing integration uses
this mechanism for assertion diagnostics.
These are advanced integration tools. Application code should normally use
typed Python and the APIs already provided by pyjs.web.dom,
pyjs.web.domx, and pyjs.web.ui.
pyjs.web.ui contains reusable custom-element components inspired by shadcn/ui
and adapted for normal Python imports, subclassing, and imperative DOM
composition. The catalog includes forms, dialogs, menus, popovers, tabs,
accordions, data display, navigation, charts, and layout primitives.
from pyjs.web.ui.button import Button
from pyjs.web.ui.card import (
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
)
profile = Card(
CardHeader(CardTitle("Profile")),
CardContent("Update your public details."),
CardFooter(Button("Save changes")),
)Reactive state is backed by custom-element attributes:
from pyjs import effect, js, state
from pyjs.web.domx import CustomElement, ref, tag
@js
class Counter(CustomElement):
count = state(0)
def __init__(self):
super().__init__()
self.value = ref(tag("output"))
tag(self, self.value)
@effect(count)
def render_count(self):
self.value.textContent = str(self.count)Effects run during server rendering. Hydration reuses that rendered result rather than repeating the initial effect in the browser.
PyJS registers a pytest plugin that can run portable tests in Python or compile them for isolated browser iframes:
python -m pytest -q
python -m pytest -q --pyjs-target=webUse pytest.mark.pyjs_web for tests that should be compiled and run in the
browser. Unmarked tests run only as ordinary Python tests;
pytest.mark.pyjs_python can make that intent explicit. A filename such as
test_browser_dialog.py is only an organizational convention and has no
implicit behavior.
Browser tests retain console output and errors. Tests that render meaningful UI retain their iframe preview, and render assertions can compare screenshots against checked-in PNG baselines.
Fresh browser/Android compilation reloads project client and test modules, not dependencies installed inside a project-local virtual environment. When Django's app registry is already initialized, registered model modules, loaded admin autodiscovery modules and their parent packages retain their identity. Restart the test process after changing those modules; unrelated client children still reload normally. Other import-time registries must tolerate reloading or stay outside client/test module imports. After each build, host modules and package bindings are restored, including on compilation errors, so Django views and model methods continue using their original imports. Build generations mutate process-global imports and must run serially.
| Command | Purpose |
|---|---|
pyjs --version |
Show the installed version |
pyjs FILE.py, pyjs -m MODULE, pyjs -c CODE |
Run with managed Node |
pyjs --src INPUT |
Print generated JavaScript without running it |
pyjs --src-all INPUT |
Include all tree-shaken dependency sources |
pyjs --perf INPUT |
Compare Python and generated-JavaScript performance |
pyjs create NAME [DESTINATION] |
Create a starter project |
pyjs serve [TARGET] |
Serve a configured project, module, or Python file |
pyjs template |
List templates registered by installed packages |
pyjs template NAME |
Print the complete materialized template source |
pyjs template NAME --src |
Print the transpiled entry-module JavaScript |
pyjs build MODULE[:ENTRY] |
Write generated JavaScript, CSS, and HTML |
pyjs docs build |
Build a static documentation site |
pyjs docs serve |
Build and serve documentation locally |
pyjs inspect URL |
Capture a screenshot and structured browser diagnostics |
pyjs npm sync [TARGET] |
Install exact reachable JavaScript dependencies |
pyjs skill |
Print version-matched instructions for coding agents |
The built-in development server is run from Python source with
pyjs.server.dev.serve(__file__); generated starters and examples already
provide this entry point.
For reproducible cross-runtime comparisons, see the performance lab.
It runs shared Python workloads on CPython, JavaScript, LLVM and JVM, alongside
handwritten JavaScript, Rust and Java references, and saves JSON with raw evidence and explicit
correctness or support failures. Open the live Python-authored UI with
uv run pyjs serve perf.app:create_app --open /perf/; no report HTML is written.
Configured docs servers can also mount live routes with
pyjs docs serve --app module:mount --open /some/path/, where mount(application)
registers routes alongside the documentation.
Web compatibility modules can declare exact npm dependencies as module
metadata. pyjs npm sync analyzes the configured application and installs only
the packages reachable from its client bundle into .pyjs/npm; no user-owned
package.json or global npm installation is required.
pyjs.web.xterm provides typed terminal options, terminal instances, input
subscriptions and the fitting addon. It pins xterm.js 6.0.0 and FitAddon
0.11.0. Call create_terminal(TerminalOptions(...)), mount it with open,
and use the async write(terminal, text) helper when output ordering matters.
Dispose subscriptions and the terminal when their host is removed. These are
browser bindings; running shell commands requires a separately authorized PTY
service. Applications that redistribute the bundle must retain the MIT notices
from the installed xterm packages.
Existing terminals expose mutable terminal.options.fontSize, fontFamily,
scrollback and cursorBlink. Assign these properties to change rendering
preferences without replacing the terminal or its input subscriptions. Call
the fitting addon's fit() after font changes to update the visible dimensions.
Reducing scrollback removes older lines from the emulator's screen buffer.
NpmImport(..., styles=("css/package.css",)) includes package-relative,
self-contained styles in the managed vendor module. Styles mount once per
package/version/path when imported in a document; imports in Node do not
require a DOM. Package styles precede application styles, so application
overrides retain their normal cascade priority. The initial validation is
conservative: CSS imports, URL/image functions and CSS escapes are rejected.
Asset bundling is not supported. Separate declarations are merged
deterministically while preserving declared stylesheet order. All namespace
aliases for a package share that order; contradictory orders are errors,
including conflicts across aliases. Each package stylesheet is emitted once.
The styles are part of vendor cache identity and are delivered by the same
development and packaging paths as other managed npm dependencies.
Synchronize an application after adding its first managed JavaScript wrapper, then build it normally.
PyJS is intentionally a typed subset of Python, and the current releases remain
pre-alpha. This section summarizes the current boundary. Run pyjs skill for
guidance matching the installed version and inspect the executable templates
with pyjs template.
- Multiple inheritance is not supported.
- Reflection, runtime monkey-patching,
eval(), and arbitrary dynamic imports are outside the compilation model. - Python standard-library and browser API coverage is incomplete; APIs are added as typed PyJS definitions.
- User-defined generic class specialization still has unsupported shapes and needs dedicated regression coverage before it is considered stable.
- Some invalid programs still produce low-level analyzer errors instead of concise diagnostics with precise source locations.
- Generated bundles include source maps back to Python. Mapping coverage and browser-runtime presentation will continue to improve.
- The built-in development server rebuilds when it starts, but does not yet trigger browser reload itself. Reloadable Django applications can opt into the development browser reload client.
- Browser-target pytest fixtures and helpers are a subset of normal pytest.
Browser tests support
pytest.raises()as a context manager, including itsmatchargument and captured exception value. - Tree shaking still has an
include=Trueescape hatch for unusual methods reached only through opaque JavaScript dispatch. - Reactive effects run immediately for each attribute mutation; updates are not currently batched.
- The bundled HTTP server and documentation server are development tools, not production servers.
- Android uses direct JVM bytecode and native framework/AndroidX APIs. Calculator and Notes demonstrate the supported subset; Notes editor parity and broader platform coverage remain in progress.
- Custom Android views can override
onDraw(Canvas)andonTouchEvent(MotionEvent)usingpyjs.android.graphicsandpyjs.android.motion.pyjs.android.gestures.ScaleGestureDetectorsupplies native pinch recognition. Callbacks inherited through native superclasses are retained without explicit Python calls. Canvas coordinates are physical pixels; use display density when sharing logical-pixel geometry with web UI. Synthetic motion events useuptime_millis(), not wall-clock time, and must be recycled after dispatch. Pointer IDs remain stable when indexes change. AndroidJVMApplication.permissionsexplicitly requests manifest permissions such as("android.permission.INTERNET",). No permissions are added by default without dependencies. AAR manifests contribute their permissions, activities, services, providers and metadata through Android's official manifest merger. Incompatible minimum SDKs, unresolved placeholders and attribute conflicts fail the build. Setmanifest_overlays=(Path(...),)for explicit application overrides using Android'stools:directives. The merger uses a cached, checksum-locked host-only classpath and JDK 21; Gradle is not needed to build an application.network_security_config="@xml/network_policy"links an app-owned XML resource fromresources; Android 7 and newer apply that policy instead of the manifest's defaultusesCleartextTraffic="false". For local HTTP testing, allow only the emulator host10.0.2.2in a development resource, not all hosts. Supply a production policy when building for deployment. See the Android network-security guide.- Static native interface factories can be declared as standalone functions
using
@jvm.native("java/util/List", name="of", interface=True). PyJS emits a small Java 8 forwarding class, keeping ordinary generated control flow on its existing verifier contract. This does not add interface default methods or arbitrary native Pythonstaticmethoddeclarations. - JVM exception handling supports ordered handlers, reraising, assertions,
and
finally, including cleanup across returns, breaks and continues. Builtin exceptions can be constructed, stored in fields or collections, passed and returned, then raised repeatedly without losing identity. They accept an optional string, integer or Boolean message. A nullable string or Boolean containingNonerenders asNone, distinct from omission. Integers retain their signed 64-bit value and Booleans use Python'sTrue/Falsespelling. Assertion messages use the same conversion and are evaluated only on failure. Float and container messages remain unsupported. Caught values supportstr(), identity andisinstance(). Native Java exceptions retain their native category:Exceptioncatches Java exceptions, whileBaseExceptionalso catches Java errors. Declare specific Java exception classes with@jvm.nativeto catch them directly. Controlled Python operations raise Python categories for invalid indexing, division by zero, negative shifts, invalid integer parsing and storage limits; arbitrary Java API failures are not relabeled. - JVM strings support code-point iteration,
ord,chr,hex,replacewith optional positional or keywordcount, and slices with a literal unit step or omitted step. Dynamic or non-unit steps remain unsupported. These operations retain the UTF-16 surrogate limitation described below. String ordering compares Unicode code points, not UTF-16 units. Chained comparisons short-circuit and evaluate each reached operand once, including nested chains and application comparison methods. - JVM Python property getters support inherited virtual dispatch and retain their declared numeric representation. This does not add property setters or properties overriding ordinary instance fields.
- JVM application functions, methods, and constructors accept keyword and keyword-only arguments while preserving source evaluation order. Defaults must be immutable literals. Variadic parameters and keyword arguments to native Java declarations remain unsupported. Overrides with keyword-only parameters must preserve parameter names, kinds, order, and defaults; incompatible runtime binding contracts are rejected. Other virtual calls using keywords or omitted defaults are rejected when reachable overrides would bind them differently. Fully supplied positional calls still permit renamed parameters, and direct child-typed calls use the child's signature.
- Plain Pydantic 2 models compile as typed JVM field containers, with
keyword-only construction, inherited fields, scalar defaults, nested models,
and supported JVM collection types. Compiler type checks apply; Pydantic
coercion and
Fieldconstraints remain server-side, not JVM validators. Custom validators/serializers, initialization hooks, aliases, generic models, mutable defaults/factories, frozen fields, and dynamic extras are rejected. Field access,instance.model_dump_json()andModel.model_validate_json(text)are supported. JSON methods use the typed API codec: finite scalars, string literals, nullable values, lists, string-key dictionaries, and acyclic models. They validate structure, not Pydantic coercion orFieldconstraints. Nested fields serialize their declared model shape, excluding subclass-only fields. Custom JSON method overrides and optional Pydantic method arguments are not supported. Constructing an unsupported-codec subclass also disables its parent's virtual JSON dump calls, even on an explicitly constructed parent. Static parent JSON factories remain usable. Field-only construction is unaffected. Model equality and dictionary dumping are not yet implemented. - Client
model_validate_json()reports malformed input and structural mismatches asValueError, including on JavaScript. JSON output rejects non-finite floats rather than replacing them with null. JavaScript JSON output currently rejects date/datetime values: those types share a client representation and cannot yet be serialized unambiguously. Ordinarymodel_dump()retains its existing date values. JavaScript honorsextra="forbid"and rejects string values for strict integer fields. JSON object keys still decode within union key schemas; an exact string key branch wins over numeric conversion. Dynamicextra="allow"models are rejected at compilation rather than losing data. JavaScript model unions prefer the successful schema retaining the most supplied model fields, including nested fields; defaults do not increase this count. Ties retain declaration order. This is not full Pydantic smart coercion. JSON object key order is not a cross-backend canonical format. pyjs.server.jvm.JVMTransportimplements the existingClientTransportHTTP interface using native Java HTTP(S), including on Android. It sends UTF-8 JSON and retains status/body for errors. Headers are explicit; redirects are not followed. Responses default to an 8 MiB byte limit. Set a finite request timeout for network operations; zero disables the connect/read timeouts. These are not total-request deadlines. Calls block the calling JVM thread despite the async interface: use an explicit worker, never Android's UI thread. Native I/O exceptions propagate. An optionalcookies=CookieManager()frompyjs.java.netprovides an isolated in-memory cookie jar without installing a global cookie handler.get()andpost_form()support session bootstrap and ordinary Django login/logout, retaining redirects so the app can inspect their status. CSRF headers remain explicit and must follow token rotation after login. For Django over HTTPS, also send an HTTPSRefererfrom the trusted server origin; a CSRF token alone is insufficient. This is the portable choice: desktop OpenJDK silently drops explicitOriginheaders. Native Java HTTP does not add these browser headers automatically. Managed cookies cannot be combined with explicit Cookie headers. Credential headers are not origin-scoped: keep a transport per trusted server/account. Do not install a process-global CookieHandler while using managed sessions: Java HTTP also consults that global handler. Requests reject one already installed rather than mix its credentials with the transport's private jar. Cookie persistence, cancellation, and lifecycle-aware delivery remain application responsibilities.- JVM clients compile ordinary
APIquery, mutation, and action methods into typed remote operations, including nested API children and inherited methods. Pass an absolute HTTP(S) API root and a finitetimeout_ms; there is no implicit browser origin for the default relative/apipath on JVM. Server operation bodies are not analyzed or emitted. Supported schemas are scalar values, string literals, nullable values, lists, string-keyed mappings, and the plain Pydantic models described above. Results are decoded by schema; Pydantic coercion and constraints remain server-side. The compiler rejects recursive schemas, excessive API graphs, streams, JSON:API resources, relationships, variadic operation parameters, and custom API initialization. Operation errors preserve their status, code, title, detail, and field path; malformed error bodies become a genericOperationErrorretaining the body. Calls still block their thread despite their async signatures. pyjs.java.json.JSONReaderprovides typed JSON tokens for schema-directed decoding without building an untyped object tree. Callfinish()after reading the root value.skip()validates unknown values with a default nesting limit of 64 containers; number tokens keep their original spelling for the schema decoder to check range and precision. Diagnostic positions count UTF-16 units.quote()produces an ASCII-escaped JSON string.integer()requires a signed 64-bit integer token;floating()accepts finite binary64 values, preserving negative zero and ordinary underflow. Both reject overflow instead of silently wrapping or producing infinity. Generated operation codecs use these tokens without an untyped object tree.- JVM dictionaries support ordinary integer, float, Boolean and reference
values with integer or reference keys. Integer keys retain signed 64-bit
precision and compare by value. Float keys, nullable numeric keys and Boolean
operands for integer-keyed dictionaries remain explicit compilation boundaries.
Iteration and live, reusable key views preserve
insertion order. Updating values does not invalidate an iterator; changing
its key set raises
RuntimeError. Scalargetrequires a matching default; omitted/Nonedefaults for scalar values remain explicit boundaries.popreturns and removes present values, evaluates explicit defaults eagerly, and raisesKeyErrorfor absent keys when no default was supplied.copycreates a shallow, insertion-ordered copy with independent keys. Dictionary subscription raisesKeyErrorfor missing keys without evaluating the mapping or key twice. PresentNonevalues remain valid. String and nullable-string keys retain Python-style quoted messages; integer keys use their decimal spelling.KeyError()andKeyError("")remain distinct. Formatting uses pinned Unicode 16 printability rather than the host JDK's Unicode version. The existing UTF-16 representation conflates adjacent high/low surrogate code points with the corresponding astral character; quoting cannot recover that distinction. Isolated surrogates are escaped. Other reference-key lookups are catchable but currently use the generic messagekey not found; their original keys remain available through.args. - JVM nullable integers, floats, and Booleans use boxed storage in parameters,
returns, fields, and collections.
Nonestays distinct from zero; equality keeps integer precision, including above2**53. Truth tests treatNoneand zero as false; NaN is true, as in Python. Narrow withis not Nonebefore arithmetic or formatting. Represented values retain their wrapping and rounding rules. Mixed numeric unions such asint | float | Noneand scalarisinstanceremain unsupported. Numeric identity checks only support comparison againstNone; nullable numeric dictionary keys remain unsupported. Explicit casts from present nullable numbers use the scalar ABI without losing integer precision. As with other static casts, callers must ensure the value is notNonebefore casting to a non-null type. pyjs.java.timebinds native ISOLocalDatevalues, device-local today and explicit fixed clocks/time zones.local_date_at(fixed_clock(...))makes calendar tests deterministic without replacing the process-wide time zone. This is a JVM API (Android API 26+), not a Pythondatetimeimplementation. Android applications using it must setmin_sdkto at least 26; compilation does not yet diagnose lower minima or add core-library desugaring.- JVM list membership supports matching integer, Boolean and string items,
including nullable strings. Floats and other mixed item domains remain
explicit unsupported boundaries.
Dictionary membership checks keys, including nullable strings/Booleans,
independently of their values. The dictionary key storage restrictions above
still apply. Membership syntax evaluates the needle before the container,
including application and declared native
__contains__methods. Explicit method calls retain receiver-first evaluation order. - JVM
and/orconditions accept mixed string, integer and Boolean operands, preserving short-circuit order in branches, loops, assertions and negation. Nullable strings treat bothNoneand the empty string as false. Non-final float, collection and custom-object operands still require explicit tests. This does not add storage for mixed primitive/reference union values. - JVM fixed tuples support local unpacking, including nested tuples, swaps, repeated targets and tuple items in loops. Unpacking into fields or indices, starred targets and variable-length inputs remains unsupported.
- JVM
tuple[T, ...]supports reference-valued items, including nullable references and nested tuples. Literals, calls, returns and fields retain identity;len, signed integer/Boolean indexing andforloops work. Primitive items, fixed-to-variable tuple conversions, slicing, repetition, membership, unpacking and structural equality remain unsupported. Equality on a reference array erased toobjectraisesTypeErrorrather than using Java array identity;isandis notremain available. Use explicitcast(object, value)when erasing reference items toobject; this does not add general primitive boxing. - JVM synchronous context managers support nested cleanup, suppression and
abrupt exits. Exceptional
__exit__calls receive a Java class token, the actual throwable and a JVM stack-trace array, not Python traceback objects. Normal exits receive threeNonevalues. Nullable Boolean results preserve the distinction betweenNone,FalseandTrue. Native exception construction requires an explicit Java constructor declaration. Python's base-exception initializer does not specify Java overloads; inherited native declarations are trusted to describe the actual Java constructor signatures. Writable chaining state, builtin literalNonemessages andUnicodeDecodeErrorpayloads remain unsupported. Explicit reraising keeps the original JVM stack trace rather than adding a Python traceback frame. Async context managers remain unsupported. - JVM managed exceptions expose read-only
.argscontaining original positional values, independently of formatted text. Keywords and defaulted parameters are excluded; a base initializer replaces the tuple without changing previously saved tuples. Original references and currently supported numeric constructor payloads retain their values, including custom float and JVM-width arguments. This does not broaden default exception text or builtin constructor signatures. Native Java exceptions have no Python argument tuple: concrete access is rejected, while widened access raisesTypeError. Tuple operations retain the limits above. Erased numeric payloads compare with Python's numeric equality, but cannot be dictionary keys yet: numeric and reference-array keys through anobjectview raiseTypeErrorinstead of using incompatible Java hashing. - Native Java file bindings and Android
Context.getFilesDir()support app-private storage. The Notes example demonstrates bounded card-order snapshots, checked replacement, failure reporting and retry without overwriting an unreadable snapshot. See its README for durability and host-platform limits. pyjs.android.json.JsonWriterwrites structured JSON through native Java character writers. UseStringWriterfor in-memory output or wrap a document provider'sContentResolver.open_output_stream(uri, "wt")in anOutputStreamWriter(stream, "UTF-8"). The resolver may returnNoneor raise a native I/O/security exception. Close owned writers and streams infinally; closingJsonWriteralso closes its underlying writer. A provider failure can leave partial output, so do not report success before close.- Android packages can set
AndroidJVMApplication.application_classto a source-definedApplicationsubclass. Initialize framework-owned state inonCreate, not Python__init__(including inherited initializers). Compiled tests select the single most-derived Application owner; unrelated owners require separate test selections. Application state lasts only for the current process, not across process termination. pyjs.java.uuidprovidesrandom_uuid()andparse_uuid()returning a nativeUUIDwithtoString(),version(), andvariant(). Parsing follows the platform'sUUID.fromStringrules, which can differ across Java and Android releases; it is not a strict canonical-text validator. Catch native parse errors withpyjs.java.errors.IllegalArgumentException.pyjs.java.concurrentprovides nativeRunnableandThread;pyjs.android.dispatchprovidesHandlerandmain_looper().Handler.post()reports queue acceptance, not completed or durable work. Keep UI state on the main thread, pass detached snapshots to workers, and acknowledge results on the main loop. These are not durable Android jobs. Compiled tests can usepyjs.android.testing.run_on_main()without a visible Activity.- Notes demonstrates serialized background saves, detached snapshots,
main-loop acknowledgements and Activity-scoped status observers.
pyjs.java.concurrent.AtomicBooleanprovides release/acquire publication for worker results; pending edits still require successful persistence to survive process termination.Handler.postDelayed(task, delay_ms)schedules on the handler's loop without an attached view; guard stale callbacks after lifecycle or account changes. pyjs.android.sqliteexposes Android's database, cursor, prepared-statement and exception APIs. Bind values instead of interpolating SQL, close cursors and statements, and pairbeginTransaction()withendTransaction()in afinallyblock. CallsetTransactionSuccessful()only after all operations succeed. These APIs block and transactions belong to one thread; application database work should run on a worker, not the UI thread. Cursor rows are limited by Android'sCursorWindow; use a statement'ssimpleQueryForString()for large scalar text instead of a cursor row.- JVM
sorted()supports lists, strings, sets and dictionary keys with stable ordering, includingreverse=True. It uses only<, leaves the input unchanged, and bounds comparisons to O(n log n). Key callbacks, other iterable kinds and fixed-width item representations remain explicit compilation boundaries. NaNs and inconsistent comparators have no guaranteed order; their output and comparison-call sequence may differ from CPython's sorting algorithm. - JVM reference-valued sets support literals, empty construction, iteration,
membership, truth, length,
add,discard,clearand shallowcopy. Numeric members, custom equality/hash, set algebra and iterable construction remain unsupported; unsupported operations fail during compilation. Structural mutation during iteration raisesRuntimeError, including same-size remove/add changes that CPython may let continue. - JVM
list(existing_list)andexisting_list.copy()copy list storage while retaining shared references to its items. Empty copies can grow normally. Source and result must have matching item storage and representation; converting other iterable kinds or item layouts remains unsupported. - JVM
list.pop()andlist.pop(index)remove and return the selected item in place, preserving aliases, shallow identity and numeric storage. Negative indices count from the end. Empty lists and out-of-range indices raiseIndexError; vacated array slots are cleared. Removing the last element is constant time; removing an earlier element shifts the following items. - JVM
math.sqrt()accepts integers, Booleans and floats. Negative inputs raiseValueError; negative zero, positive infinity and NaN retain their floating-point behavior. The implementation uses Java's native square root after a Python-compatible domain check. Domain errors currently use the messagemath domain error; wording differs from newer CPython releases. - JVM custom Python exception classes retain inherited catches, typed fields,
owned identity, custom initializers and module-scoped class identity.
String/integer/Boolean positional arguments supply text without
super(); keywords and defaulted parameters do not become that original text.super().__init__(message)can replace the text. Custom__str__methods keep virtual dispatch through base-typed and union-typed references. Field-only non-scalar arguments remain valid. Consuming their unsupported default text raises a focusedTypeError; replacing it with a scalar message or providing a custom formatter avoids that unsupported operation. Java logging preserves the exception and stack trace if formatting fails, using<exception str() failed>as its message. Explicitly raised custom exceptions retain their formatters even without a Pythonstr()call. The same applies to exceptions passed to native Java calls, exposed through native views, stored in native fields, or returned by Java callbacks, including typed map contents,cast(object, ...)and nullable native views. Conditional values retain a shared custom exception base when returning through that base. Raw sibling unions also retain formatting in conditional locals, list items and multi-type exception handlers. Optional exceptions format absent values asNonethroughstr(), f-strings and no-argument__str__()calls; present values keep custom dispatch. Python-authored subclasses of native Java throwables keep their formatters through these Python text operations, including mixed exception unions. Their JavagetMessage()behavior is unchanged.raise error from causeretains cause identity for Python-managed exceptions;from Noneclears it and sets suppression. Read-only__cause__and__suppress_context__expose this state. Replacing causes, self-cycles, cleanup and reraising preserve Python semantics; Java stack traces show explicit causes. Native Java failures may be causes, but cannot receive Python-managed chaining state: concrete native targets are rejected at compile time, and widened targets raiseTypeError. Foreign exceptions expose their existing Java cause and report no Python suppression flag. On managed exceptions, JavainitCause()remains visible until Python explicitly sets or clears the cause; that Python state then takes priority. Use exception instances rather than classes for the raise/cause operands. Read-only__context__retains the active handled exception when a Python raise or runtime check fails, including in called helpers and cleanup.from Nonesuppresses reporting without discarding this context. Nested handlers restore their enclosing state on return and unwind, and separate threads never share active handlers. Reusing exceptions breaks implicit context cycles without altering explicit causes. Native Java failures may be context values, but expose no Python context themselves.pyjs.java.format_exception(error)renders causes or unsuppressed context oldest-first, with Java frames, generated class names and native suppressed details. Managed exceptions use that rendering in all JavaprintStackTraceoverloads too. Native roots keep Java's printer; useformat_exceptionto include Python context beneath them. Reports handle cycles iteratively and do not mutate exception relationships or stack traces. Native suppressed subtrees retain Java's cause traversal. Invalid managed formatters render<exception str() failed>without losing the original failure. Reports use the host line separator and preserve PrintWriter auto-flush settings. For uncaught chains, Java's thread prefix precedes the oldest entry; the escaping exception appears last, following Python chain order. Assignment to chaining attributes remains unsupported. Explicit optional calls require string-compatible formatter results; narrow the receiver before consuming a differently typed custom result.objectlocals retain existing inferred union/collection storage, but cannot erase exception values or exception-bearing containers. Other plainobjectinitializers remain unsupported unless the value is already erased, such as an argument-tuple item or explicit cast. Use explicit casts to expose supported reference values to Java. Explicit__str__()calls through base views require compatible override result types; narrow to the concrete type for a differently typed result. Localsuper()aliases keep lexical dispatch; mixed or escaping proxies receive a focused compilation diagnostic. Python methods are isolated from native Throwable method names. Custom__new__, unbound builtin initialization and multiple exception bases remain unsupported. Construct a custom-initializer class before raising it.
Run pyjs skill for the installed-version guidance and pyjs template for
complete executable examples.
Run pyjs lsp --root PATH for experimental, versioned module snapshots and
compiler-backed completion and signature queries. Pass
--python /path/to/python to execute analysis with project dependencies in a
separate interpreter containing PyJS. See the
editor analysis protocol for source synchronization,
execution behavior, and current limitations.
The development environment, repository structure, compiler contracts, and verification workflow are documented in CONTRIBUTING.md.
PyJS is distributed under the BSD 3-Clause License.