diff --git a/docs/api.md b/docs/api.md
deleted file mode 100644
index 21327b02..00000000
--- a/docs/api.md
+++ /dev/null
@@ -1,1287 +0,0 @@
-# Built-in APIs
-
-PyScript makes available convenience objects, functions and attributes.
-
-In Python this is done via the builtin `pyscript` module:
-
-```python title="Accessing the document object via the pyscript module"
-from pyscript import document
-```
-
-In HTML this is done via `py-*` and `mpy-*` attributes (depending on the
-interpreter you're using):
-
-```html title="An example of a py-click handler"
-Click me
-```
-
-These APIs will work with both Pyodide and Micropython in exactly the same way.
-
-!!! info
-
- Both Pyodide and MicroPython provide access to two further lower-level
- APIs:
-
- * Access to
- [JavaScript's `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis)
- via importing the `js` module: `import js` (now `js` is a proxy for
- `globalThis` in which all native JavaScript based browser APIs are
- found).
- * Access to interpreter specific versions of utilities and the foreign
- function interface. Since these are different for each interpreter, and
- beyond the scope of PyScript's own documentation, please check each
- project's documentation
- ([Pyodide](https://pyodide.org/en/stable/usage/api-reference.html) /
- [MicroPython](https://docs.micropython.org/en/latest/)) for details of
- these lower-level APIs.
-
-PyScript can run in two contexts: the main browser thread, or on a web worker.
-The following three categories of API functionality explain features that are
-common for both main thread and worker, main thread only, and worker only. Most
-features work in both contexts in exactly the same manner, but please be aware
-that some are specific to either the main thread or a worker context.
-
-## Common features
-
-These Python objects / functions are available in both the main thread and in
-code running on a web worker:
-
-### `pyscript.config`
-
-A Python dictionary representing the configuration for the interpreter.
-
-```python title="Reading the current configuration."
-from pyscript import config
-
-
-# It's just a dict.
-print(config.get("files"))
-# This will be either "mpy" or "py" depending on the current interpreter.
-print(config["type"])
-```
-
-!!! info
-
- The `config` object will always include a `type` attribute set to either
- `mpy` or `py`, to indicate which version of Python your code is currently
- running in.
-
-!!! warning
-
- Changing the `config` dictionary at runtime has no effect on the actual
- configuration.
-
- It's just a convenience to **read the configuration** at run time.
-
-### `pyscript.current_target`
-
-A utility function to retrieve the unique identifier of the element used
-to display content. If the element is not a `
-```
-
-!!! Note
-
- The return value of `current_target()` always references a visible element
- on the page, **not** at the current `
- ```
-
- Then use the standard `document.getElementById(script_id)` function to
- return a reference to it in your code.
-
-### `pyscript.display`
-
-A function used to display content. The function is intelligent enough to
-introspect the object[s] it is passed and work out how to correctly display the
-object[s] in the web page based on the following mime types:
-
-* `text/plain` to show the content as text
-* `text/html` to show the content as *HTML*
-* `image/png` to show the content as ` `
-* `image/jpeg` to show the content as ` `
-* `image/svg+xml` to show the content as ``
-* `application/json` to show the content as *JSON*
-* `application/javascript` to put the content in `
- PyScript
--->
-
-
-
-
-
-
-
-
-
-```
-
-### `pyscript.document`
-
-On both main and worker threads, this object is a proxy for the web page's
-[document object](https://developer.mozilla.org/en-US/docs/Web/API/Document).
-The `document` is a representation of the
-[DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Using_the_Document_Object_Model)
-and can be used to read or manipulate the content of the web page.
-
-### `pyscript.fetch`
-
-A common task is to `fetch` data from the web via HTTP requests. The
-`pyscript.fetch` function provides a uniform way to achieve this in both
-Pyodide and MicroPython. It is closely modelled on the
-[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) found
-in browsers with some important Pythonic differences.
-
-The simple use case is to pass in a URL and `await` the response. If this
-request is in a function, that function should also be defined as `async`.
-
-```python title="A simple HTTP GET with pyscript.fetch"
-from pyscript import fetch
-
-
-response = await fetch("https://example.com")
-if response.ok:
- data = await response.text()
-else:
- print(response.status)
-```
-
-The object returned from an `await fetch` call will have attributes that
-correspond to the
-[JavaScript response object](https://developer.mozilla.org/en-US/docs/Web/API/Response).
-This is useful for getting response codes, headers and other metadata before
-processing the response's data.
-
-Alternatively, rather than using a double `await` (one to get the response, the
-other to grab the data), it's possible to chain the calls into a single
-`await` like this:
-
-```python title="A simple HTTP GET as a single await"
-from pyscript import fetch
-
-data = await fetch("https://example.com").text()
-```
-
-The following awaitable methods are available to you to access the data
-returned from the server:
-
-* `arrayBuffer()` returns a Python
- [memoryview](https://docs.python.org/3/library/stdtypes.html#memoryview) of
- the response. This is equivalent to the
- [`arrayBuffer()` method](https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer)
- in the browser based `fetch` API.
-* `blob()` returns a JavaScript
- [`blob`](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob)
- version of the response. This is equivalent to the
- [`blob()` method](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob)
- in the browser based `fetch` API.
-* `bytearray()` returns a Python
- [`bytearray`](https://docs.python.org/3/library/stdtypes.html#bytearray)
- version of the response.
-* `json()` returns a Python datastructure representing a JSON serialised
- payload in the response.
-* `text()` returns a Python string version of the response.
-
-The underlying browser `fetch` API has
-[many request options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options)
-that you should simply pass in as keyword arguments like this:
-
-```python title="Supplying request options."
-from pyscript import fetch
-
-
-result = await fetch("https://example.com", method="POST", body="HELLO").text()
-```
-
-!!! Danger
-
- You may encounter
- [CORS](https://developer.mozilla.org/en-US/docs/Glossary/CORS)
- errors (especially with reference to a missing
- [Access-Control-Allow-Origin header](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin).
-
- This is a security feature of modern browsers where the site to which you
- are making a request **will not process a request from a site hosted at
- another domain**.
-
- For example, if your PyScript app is hosted under `example.com` and you
- make a request to `bbc.co.uk` (who don't allow requests from other domains)
- then you'll encounter this sort of CORS related error.
-
- There is nothing PyScript can do about this problem (it's a feature, not a
- bug). However, you could use a pass-through proxy service to get around
- this limitation (i.e. the proxy service makes the call on your behalf).
-
-### `pyscript.ffi`
-
-The `pyscript.ffi` namespace contains foreign function interface (FFI) methods
-that work in both Pyodide and MicroPython.
-
-#### `pyscript.ffi.create_proxy`
-
-A utility function explicitly for when a callback function is added via an
-event listener. It ensures the function still exists beyond the assignment of
-the function to an event. Should you not `create_proxy` around the callback
-function, it will be immediately garbage collected after being bound to the
-event.
-
-!!! warning
-
- There is some technical complexity to this situation, and we have attempted
- to create a mechanism where `create_proxy` is never needed.
-
- *Pyodide* expects the created proxy to be explicitly destroyed when it's
- not needed / used anymore. However, the underlying `proxy.destroy()` method
- has not been implemented in *MicroPython* (yet).
-
- To simplify this situation and automatically destroy proxies based on
- JavaScript memory management (garbage collection) heuristics, we have
- introduced an **experimental flag**:
-
- ```toml
- experimental_create_proxy = "auto"
- ```
-
- This flag ensures the proxy creation and destruction process is managed for
- you. When using this flag you should never need to explicitly call
- `create_proxy`.
-
-The technical details of how this works are
-[described here](../user-guide/ffi#create_proxy).
-
-#### `pyscript.ffi.to_js`
-
-A utility function to convert Python references into their JavaScript
-equivalents. For example, a Python dictionary is converted into a JavaScript
-object literal (rather than a JavaScript `Map`), unless a `dict_converter`
-is explicitly specified and the runtime is Pyodide.
-
-The technical details of how this works are [described here](../user-guide/ffi#to_js).
-
-### `pyscript.fs`
-
-!!! danger
-
- This API only works in Chromium based browsers.
-
-An API for mounting the user's local filesystem to a designated directory in
-the browser's virtual filesystem. Please see
-[the filesystem](../user-guide/filesystem) section of the user-guide for more
-information.
-
-#### `pyscript.fs.mount`
-
-Mount a directory on the user's local filesystem into the browser's virtual
-filesystem. If no previous
-[transient user activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation)
-has taken place, this function will result in a minimalist dialog to provide
-the required transient user activation.
-
-This asynchronous function takes four arguments:
-
-* `path` (required) - indicating the location on the in-browser filesystem to
- which the user selected directory from the local filesystem will be mounted.
-* `mode` (default: `"readwrite"`) - indicates how the code may interact with
- the mounted filesystem. May also be just `"read"` for read-only access.
-* `id` (default: `"pyscript"`) - indicate a unique name for the handler
- associated with a directory on the user's local filesystem. This allows users
- to select different folders and mount them at the same path in the
- virtual filesystem.
-* `root` (default: `""`) - a hint to the browser for where to start picking the
- path that should be mounted in Python. Valid values are: `desktop`,
- `documents`, `downloads`, `music`, `pictures` or `videos` as per
- [web standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#startin).
-
-```python title="Mount a local directory to the '/local' directory in the browser's virtual filesystem"
-from pyscript import fs
-
-
-# May ask for permission from the user, and select the local target.
-await fs.mount("/local")
-```
-
-If the call to `fs.mount` happens after a click or other transient event, the
-confirmation dialog will not be shown.
-
-```python title="Mounting without a transient event dialog."
-from pyscript import fs
-
-
-async def handler(event):
- """
- The click event that calls this handler is already a transient event.
- """
- await fs.mount("/local")
-
-
-my_button.onclick = handler
-```
-
-#### `pyscript.fs.sync`
-
-Given a named `path` for a mount point on the browser's virtual filesystem,
-asynchronously ensure the virtual and local directories are synchronised (i.e.
-all changes made in the browser's mounted filesystem, are propagated to the
-user's local filesystem).
-
-```python title="Synchronise the virtual and local filesystems."
-await fs.sync("/local")
-```
-
-#### `pyscript.fs.unmount`
-
-Asynchronously unmount the named `path` from the browser's virtual filesystem
-after ensuring content is synchronized. This will free up memory and allow you
-to re-use the path to mount a different directory.
-
-```python title="Unmount from the virtual filesystem."
-await fs.unmount("/local")
-```
-
-### `pyscript.js_modules`
-
-It is possible to [define JavaScript modules to use within your Python code](../user-guide/configuration#javascript-modules).
-
-Such named modules will always then be available under the
-`pyscript.js_modules` namespace.
-
-!!! warning
-
- Please see the documentation (linked above) about restrictions and gotchas
- when configuring how JavaScript modules are made available to PyScript.
-
-### `pyscript.media`
-
-The `pyscript.media` namespace provides classes and functions for interacting
-with media devices and streams in a web browser. This module enables you to work
-with cameras, microphones, and other media input/output devices directly from
-Python code.
-
-#### `pyscript.media.Device`
-
-A class that represents a media input or output device, such as a microphone,
-camera, or headset.
-
-```python title="Creating a Device object"
-from pyscript.media import Device, list_devices
-
-# List all available media devices
-devices = await list_devices()
-# Get the first available device
-my_device = devices[0]
-```
-
-The `Device` class has the following properties:
-
-* `id` - a unique string identifier for the represented device.
-* `group` - a string group identifier for devices belonging to the same physical device.
-* `kind` - an enumerated value: "videoinput", "audioinput", or "audiooutput".
-* `label` - a string describing the device (e.g., "External USB Webcam").
-
-The `Device` class also provides the following methods:
-
-##### `Device.load(audio=False, video=True)`
-
-A class method that loads a media stream with the specified options.
-
-```python title="Loading a media stream"
-# Load a video stream (default)
-stream = await Device.load()
-
-# Load an audio stream only
-stream = await Device.load(audio=True, video=False)
-
-# Load with specific video constraints
-stream = await Device.load(video={"width": 1280, "height": 720})
-```
-
-Parameters:
-* `audio` (bool, default: False) - Whether to include audio in the stream.
-* `video` (bool or dict, default: True) - Whether to include video in the
- stream. Can also be a dictionary of video constraints.
-
-Returns:
-* A media stream object that can be used with HTML media elements.
-
-##### `get_stream()`
-
-An instance method that gets a media stream from this specific device.
-
-```python title="Getting a stream from a specific device"
-# Find a video input device
-video_devices = [d for d in devices if d.kind == "videoinput"]
-if video_devices:
- # Get a stream from the first video device
- stream = await video_devices[0].get_stream()
-```
-
-Returns:
-* A media stream object from the specific device.
-
-#### `pyscript.media.list_devices()`
-
-An async function that returns a list of all currently available media input and
-output devices.
-
-```python title="Listing all media devices"
-from pyscript.media import list_devices
-
-devices = await list_devices()
-for device in devices:
- print(f"Device: {device.label}, Kind: {device.kind}")
-```
-
-Returns:
-* A list of `Device` objects representing the available media devices.
-
-!!! Note
-
- The returned list will omit any devices that are blocked by the document
- Permission Policy or for which the user has not granted permission.
-
-### Simple Example
-
-```python title="Basic camera access"
-from pyscript import document
-from pyscript.media import Device
-
-async def init_camera():
- # Get a video stream
- stream = await Device.load(video=True)
-
- # Set the stream as the source for a video element
- video_el = document.getElementById("camera")
- video_el.srcObject = stream
-
-# Initialize the camera
-init_camera()
-```
-
-!!! warning
-
- Using media devices requires appropriate permissions from the user.
- Browsers will typically show a permission dialog when `list_devices()` or
- `Device.load()` is called.
-
-### `pyscript.storage`
-
-The `pyscript.storage` API wraps the browser's built-in
-[IndexDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
-persistent storage in a synchronous Pythonic API.
-
-!!! info
-
- The storage API is persistent per user tab, page, or domain, in the same
- way IndexedDB persists.
-
- This API **is not** saving files in the interpreter's virtual file system
- nor onto the user's hard drive.
-
-```python
-from pyscript import storage
-
-
-# Each store must have a meaningful name.
-store = await storage("my-storage-name")
-
-# store is a dictionary and can now be used as such.
-```
-
-The returned dictionary automatically loads the current state of the referenced
-IndexDB. All changes are automatically queued in the background.
-
-```python
-# This is a write operation.
-store["key"] = value
-
-# This is also a write operation (it changes the stored data).
-del store["key"]
-```
-
-Should you wish to be certain changes have been synchronized to the underlying
-IndexDB, just `await store.sync()`.
-
-Common types of value can be stored via this API: `bool`, `float`, `int`, `str`
-and `None`. In addition, data structures like `list`, `dict` and `tuple` can
-be stored.
-
-!!! warning
-
- Because of the way the underlying data structure are stored in IndexDB,
- a Python `tuple` will always be returned as a Python `list`.
-
-It is even possible to store arbitrary data via a `bytearray` or
-`memoryview` object. However, there is a limitation that **such values must be
-stored as a single key/value pair, and not as part of a nested data
-structure**.
-
-Sometimes you may need to modify the behaviour of the `dict` like object
-returned by `pyscript.storage`. To do this, create a new class that inherits
-from `pyscript.Storage`, then pass in your class to `pyscript.storage` as the
-`storage_class` argument:
-
-```python
-from pyscript import window, storage, Storage
-
-
-class MyStorage(Storage):
-
- def __setitem__(self, key, value):
- super().__setitem__(key, value)
- window.console.log(key, value)
- ...
-
-
-store = await storage("my-data-store", storage_class=MyStorage)
-
-# The store object is now an instance of MyStorage.
-```
-
-### `@pyscript/core/donkey`
-
-Sometimes you need an asynchronous Python worker ready and waiting to evaluate
-any code on your behalf. This is the concept behind the JavaScript "donkey". We
-couldn't think of a better way than "donkey" to describe something that is easy
-to understand and shoulders the burden without complaint. This feature
-means you're able to use PyScript without resorting to specialised
-`
-
-
-```
-
-### `pyscript.RUNNING_IN_WORKER`
-
-This constant flag is `True` when the current code is running within a
-*worker*. It is `False` when the code is running within the *main* thread.
-
-### `pyscript.WebSocket`
-
-If a `pyscript.fetch` results in a call and response HTTP interaction with a
-web server, the `pyscript.Websocket` class provides a way to use
-[websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
-for two-way sending and receiving of data via a long term connection with a
-web server.
-
-PyScript's implementation, available in both the main thread and a web worker,
-closely follows the browser's own
-[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) class.
-
-This class accepts the following named arguments:
-
-* A `url` pointing at the _ws_ or _wss_ address. E.g.:
- `WebSocket(url="ws://localhost:5037/")`
-* Some `protocols`, an optional string or a list of strings as
- [described here](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#parameters).
-
-The `WebSocket` class also provides these convenient static constants:
-
-* `WebSocket.CONNECTING` (`0`) - the `ws.readyState` value when a web socket
- has just been created.
-* `WebSocket.OPEN` (`1`) - the `ws.readyState` value once the socket is open.
-* `WebSocket.CLOSING` (`2`) - the `ws.readyState` after `ws.close()` is
- explicitly invoked to stop the connection.
-* `WebSocket.CLOSED` (`3`) - the `ws.readyState` once closed.
-
-A `WebSocket` instance has only 2 methods:
-
-* `ws.send(data)` - where `data` is either a string or a Python buffer,
- automatically converted into a JavaScript typed array. This sends data via
- the socket to the connected web server.
-* `ws.close(code=0, reason="because")` - which optionally accepts `code` and
- `reason` as named arguments to signal some specific status or cause for
- closing the web socket. Otherwise `ws.close()` works with the default
- standard values.
-
-A `WebSocket` instance also has the fields that the JavaScript
-`WebSocket` instance will have:
-
-* [binaryType](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType) -
- the type of binary data being received over the WebSocket connection.
-* [bufferedAmount](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount) -
- a read-only property that returns the number of bytes of data that have been
- queued using calls to `send()` but not yet transmitted to the network.
-* [extensions](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/extensions) -
- a read-only property that returns the extensions selected by the server.
-* [protocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/protocol) -
- a read-only property that returns the name of the sub-protocol the server
- selected.
-* [readyState](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) -
- a read-only property that returns the current state of the WebSocket
- connection as one of the `WebSocket` static constants (`CONNECTING`, `OPEN`,
- etc...).
-* [url](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/url) -
- a read-only property that returns the absolute URL of the `WebSocket`
- instance.
-
-A `WebSocket` instance can have the following listeners. Directly attach
-handler functions to them. Such functions will always receive a single
-`event` object.
-
-* [onclose](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event) -
- fired when the `WebSocket`'s connection is closed.
-* [onerror](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event) -
- fired when the connection is closed due to an error.
-* [onmessage](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event) -
- fired when data is received via the `WebSocket`. If the `event.data` is a
- JavaScript typed array instead of a string, the reference it will point
- directly to a _memoryview_ of the underlying `bytearray` data.
-* [onopen](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event) -
- fired when the connection is opened.
-
-The following code demonstrates a `pyscript.WebSocket` in action.
-
-```html
-
-```
-
-!!! info
-
- It's also possible to pass in any handler functions as named arguments when
- you instantiate the `pyscript.WebSocket` class:
-
- ```python
- from pyscript import WebSocket
-
-
- def onmessage(event):
- print(event.type, event.data)
- ws.close()
-
-
- ws = WebSocket(url="ws://example.com/socket", onmessage=onmessage)
- ```
-
-### `pyscript.js_import`
-
-If a JavaScript module is only needed under certain circumstances, we provide
-an asynchronous way to import packages that were not originally referenced in
-your configuration.
-
-```html title="A pyscript.js_import example."
-
-```
-
-The `py_import` call returns an asynchronous tuple containing the Python
-modules provided by the packages referenced as string arguments.
-
-## Main-thread only features
-
-### `pyscript.PyWorker`
-
-A class used to instantiate a new worker from within Python.
-
-!!! Note
-
- Sometimes we disambiguate between interpreters through naming conventions
- (e.g. `py` or `mpy`).
-
- However, this class is always `PyWorker` and **the desired interpreter
- MUST be specified via a `type` option**. Valid values for the type of
- interpreter are either `micropython` or `pyodide`.
-
-The following fragments demonstrate how to evaluate the file `worker.py` on a
-new worker from within Python.
-
-```python title="worker.py - the file to run in the worker."
-from pyscript import RUNNING_IN_WORKER, display, sync
-
-display("Hello World", target="output", append=True)
-
-# will log into devtools console
-print(RUNNING_IN_WORKER) # True
-print("sleeping")
-sync.sleep(1)
-print("awake")
-```
-
-```python title="main.py - starts a new worker in Python."
-from pyscript import PyWorker
-
-# type MUST be either `micropython` or `pyodide`
-PyWorker("worker.py", type="micropython")
-```
-
-```html title="The HTML context for the worker."
-
-```
-
-While over on the main thread, this fragment of MicroPython will be able to
-access the worker's `version` function via the `workers` reference:
-
-```html
-
-```
-
-Importantly, the `workers` reference will **NOT** provide a list of
-known workers, but will only `await` for a reference to a named worker
-(resolving when the worker is ready). This is because the timing of worker
-startup is not deterministic.
-
-Should you wish to await for all workers on the page at load time, it's
-possible to loop over matching elements in the document like this:
-
-```html
-
-```
-
-## Worker only features
-
-### `pyscript.sync`
-
-A function used to pass serializable data from workers to the main thread.
-
-Imagine you have this code on the main thread:
-
-```python title="Python code on the main thread"
-from pyscript import PyWorker
-
-def hello(name="world"):
- display(f"Hello, {name}")
-
-worker = PyWorker("./worker.py")
-worker.sync.hello = hello
-```
-
-In the code on the worker, you can pass data back to handler functions like
-this:
-
-```python title="Pass data back to the main thread from a worker"
-from pyscript import sync
-
-sync.hello("PyScript")
-```
-
-## HTML attributes
-
-As a convenience, and to ensure backwards compatibility, PyScript allows the
-use of inline event handlers via custom HTML attributes.
-
-!!! warning
-
- This classic pattern of coding (inline event handlers) is no longer
- considered good practice in web development circles.
-
- We include this behaviour for historic reasons, but the folks at
- Mozilla [have a good explanation](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#inline_event_handlers_%E2%80%94_dont_use_these)
- of why this is currently considered bad practice.
-
-These attributes, expressed as `py-*` or `mpy-*` attributes of an HTML element,
-reference the name of a Python function to run when the event is fired. You
-should replace the `*` with the _actual name of an event_ (e.g. `py-click` or
-`mpy-click`). This is similar to how all
-[event handlers on elements](https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects)
-start with `on` in standard HTML (e.g. `onclick`). The rule of thumb is to
-simply replace `on` with `py-` or `mpy-` and then reference the name of a
-Python function.
-
-```html title="A py-click event on an HTML button element."
-Click me!
-```
-
-```python title="The related Python function."
-from pyscript import window
-
-
-def handle_click(event):
- """
- Simply log the click event to the browser's console.
- """
- window.console.log(event)
-```
-
-Under the hood, the [`pyscript.when`](#pyscriptwhen) decorator is used to
-enable this behaviour.
-
-!!! note
-
- In earlier versions of PyScript, the value associated with the attribute
- was simply evaluated by the Python interpreter. This was unsafe:
- manipulation of the attribute's value could have resulted in the evaluation
- of arbitrary code.
-
- This is why we changed to the current behaviour: just supply the name
- of the Python function to be evaluated, and PyScript will do this safely.
diff --git a/docs/api/context.md b/docs/api/context.md
new file mode 100644
index 00000000..635214e7
--- /dev/null
+++ b/docs/api/context.md
@@ -0,0 +1,3 @@
+# `pyscript.context`
+
+::: pyscript.context
diff --git a/docs/api/display.md b/docs/api/display.md
new file mode 100644
index 00000000..fb986868
--- /dev/null
+++ b/docs/api/display.md
@@ -0,0 +1,3 @@
+# `pyscript.display`
+
+::: pyscript.display
diff --git a/docs/api/events.md b/docs/api/events.md
new file mode 100644
index 00000000..061c4aec
--- /dev/null
+++ b/docs/api/events.md
@@ -0,0 +1,3 @@
+# `pyscript.event`
+
+::: pyscript.events
diff --git a/docs/api/fetch.md b/docs/api/fetch.md
new file mode 100644
index 00000000..c57937f3
--- /dev/null
+++ b/docs/api/fetch.md
@@ -0,0 +1,3 @@
+# `pyscript.fetch`
+
+::: pyscript.fetch
diff --git a/docs/api/ffi.md b/docs/api/ffi.md
new file mode 100644
index 00000000..69b5472b
--- /dev/null
+++ b/docs/api/ffi.md
@@ -0,0 +1,3 @@
+# `pyscript.ffi`
+
+::: pyscript.ffi
diff --git a/docs/api/flatted.md b/docs/api/flatted.md
new file mode 100644
index 00000000..e8052ff7
--- /dev/null
+++ b/docs/api/flatted.md
@@ -0,0 +1,3 @@
+# `pyscript.flatted`
+
+::: pyscript.flatted
diff --git a/docs/api/fs.md b/docs/api/fs.md
new file mode 100644
index 00000000..6ef3363e
--- /dev/null
+++ b/docs/api/fs.md
@@ -0,0 +1,3 @@
+# `pyscript.fs`
+
+::: pyscript.fs
diff --git a/docs/api/init.md b/docs/api/init.md
new file mode 100644
index 00000000..2da87b09
--- /dev/null
+++ b/docs/api/init.md
@@ -0,0 +1,14 @@
+# The `pyscript` API
+
+!!! important
+
+ These API docs are auto-generated from our source code. To suggest
+ changes or report errors, please do so via
+ [our GitHub repository](https://github.com/pyscript/pyscript). The
+ source code for these APIs
+ [is found here](https://github.com/pyscript/pyscript/tree/main/core/src/stdlib/pyscript)
+ in our repository.
+
+::: pyscript
+ options:
+ show_root_heading: false
diff --git a/docs/api/media.md b/docs/api/media.md
new file mode 100644
index 00000000..1b197777
--- /dev/null
+++ b/docs/api/media.md
@@ -0,0 +1,3 @@
+# `pyscript.media`
+
+::: pyscript.media
diff --git a/docs/api/storage.md b/docs/api/storage.md
new file mode 100644
index 00000000..053df3c3
--- /dev/null
+++ b/docs/api/storage.md
@@ -0,0 +1,3 @@
+# `pyscript.storage`
+
+::: pyscript.storage
diff --git a/docs/api/util.md b/docs/api/util.md
new file mode 100644
index 00000000..d7e7db48
--- /dev/null
+++ b/docs/api/util.md
@@ -0,0 +1,3 @@
+# `pyscript.util`
+
+::: pyscript.util
diff --git a/docs/api/web.md b/docs/api/web.md
new file mode 100644
index 00000000..b1a3c1c3
--- /dev/null
+++ b/docs/api/web.md
@@ -0,0 +1,18 @@
+# `pyscript.web`
+
+::: pyscript.web
+ options:
+ members:
+ - page
+ - Element
+ - ContainerElement
+ - ElementCollection
+ - Classes
+ - Style
+ - HasOptions
+ - Options
+ - Page
+ - canvas
+ - video
+ - CONTAINER_TAGS
+ - VOID_TAGS
diff --git a/docs/api/websocket.md b/docs/api/websocket.md
new file mode 100644
index 00000000..093fda85
--- /dev/null
+++ b/docs/api/websocket.md
@@ -0,0 +1,3 @@
+# `pyscript.websocket`
+
+::: pyscript.websocket
diff --git a/docs/api/workers.md b/docs/api/workers.md
new file mode 100644
index 00000000..2930a8d7
--- /dev/null
+++ b/docs/api/workers.md
@@ -0,0 +1,3 @@
+# `pyscript.workers`
+
+::: pyscript.workers
diff --git a/docs/beginning-pyscript.md b/docs/beginning-pyscript.md
index 3ab7b021..3b850e26 100644
--- a/docs/beginning-pyscript.md
+++ b/docs/beginning-pyscript.md
@@ -1,45 +1,35 @@
# Beginning PyScript
-PyScript is a platform for running
+PyScript is an open source platform for
Python
-in modern web browsers.
+in the browser.
-Create apps with a
-PyScript development environment :
-write code, curate the project's assets, and test your application.
+Write code , curate the
+project's assets, and test your application just like you normally would.
-To distribute a PyScript application, host it on the web, then click
+However, to distribute a PyScript application, host it on the web, then click
on the link to your application. PyScript and the browser do the rest.
-This page covers these core aspects of PyScript in a beginner friendly manner.
-We only assume you know how to use a browser and edit text.
-
-!!! note
+Simple!
- The easiest way to get a PyScript development environment and hosting, is
- to use [pyscript.com](https://pyscript.com) in your browser.
-
- It is a free service that helps you create new projects from templates, and
- then edit, preview and deploy your apps with a unique link.
-
- While the core features of [pyscript.com](https://pyscript.com) will always be
- free, additional paid-for capabilities directly support and sustain the
- PyScript open source project. Commercial and educational support is also
- available.
+Now read on for a beginner-friendly tour of the core aspects of PyScript.
+We only assume you know how to use a browser and edit text.
## An application
-All PyScript applications need three things:
+Usually, PyScript applications need just three things:
1. An `index.html` file that is served to your browser.
2. A description of the Python environment in which your application will run.
- This is usually specified by a `pyscript.json` or `pyscript.toml` file.
+ This is usually specified by a `settings.json` or `settings.toml`
+ configuration file (the filename name doesn't matter, but you just need to
+ know you can configure your Python environment).
3. Python code (usually in a file called something like `main.py`) that defines
how your application works.
Create these files with your favourite code editor on your local file system.
-Alternatively, [pyscript.com](https://pyscript.com) will take away all the pain
-of organising, previewing and deploying your application.
+Alternatively, services like [pyscript.com](https://pyscript.com) will take
+away all the pain of organising, previewing and deploying your application.
If you're using your local file system, you'll need a way to view your
application in your browser. If you already have Python installed on
@@ -54,6 +44,7 @@ Point your browser at [http://localhost:8000](localhost:8000). Remember to
refresh the page (`CTRL-R`) to see any updates you may have made.
!!! note
+
If you're using [VSCode](https://code.visualstudio.com/) as your editor,
the
[Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)
@@ -64,30 +55,30 @@ refresh the page (`CTRL-R`) to see any updates you may have made.
[PyScript aware "CodeSpace"](https://github.com/ntoll/codespaces-project-template-pyscript/)
(just follow the instructions in the README file).
-If you decide to use [pyscript.com](https://pyscript.com) (recommended for
-first steps), once signed in, create a new project by pressing the "+" button
-on the left hand side below the site's logo. You'll be presented with a page
-containing three columns (listing your files, showing your code and previewing
-the app). The "save" and "run" buttons do exactly what you'd expect.
+If you decide to use [pyscript.com](https://pyscript.com), once signed in,
+create a new project by pressing the "+" button on the left-hand side below
+the site's logo. You'll be presented with a page containing three columns
+(listing your files, showing your code and previewing the app). The "save"
+and "run" buttons do exactly what you'd expect.

Let's build a simple PyScript application that translates English 🇬🇧 into
-Pirate 🏴☠️ speak. In order to do this we'll make use of the
+Pirate 🏴☠️ speak. In order to do this we'll make use of the
[arrr](https://arrr.readthedocs.io/en/latest/) library. By building this app
you'll be introduced to all the core concepts of PyScript at an introductory
level.
You can see this application embedded into the page below (try it out!):
-
+
Let's explore each of the three files that make this app work.
-### pyscript.json
+### `pyscript.json`
This file tells PyScript and your browser about various
-[configurable aspects](../user-guide/configuration)
+[configurable aspects](user-guide/configuration.md)
of your application. Put simply, it tells PyScript what it needs in order to run
your application. The only thing we need to show is that we require the third
party `arrr` module to do the
@@ -96,19 +87,29 @@ party `arrr` module to do the
We do this by putting `arrr` as the single entry in a list of required
`packages`, so the content of `pyscript.json` looks like this:
-``` json title="pyscript.json"
+```json title="pyscript.json"
{
"packages": ["arrr"]
}
```
-### index.html
+It doesn't need to be called `pyscript.json`, but it must be either a `json`
+or `toml` file containing valid PyScript configuration.
+
+!!! info
+
+ Want to learn more about configuration options? The
+ [configuration guide](user-guide/configuration.md) covers everything from
+ specifying Python packages to customising how PyScript loads and runs.
+
+### `index.html`
Next we come to the `index.html` file that is first served to your browser.
To start out, we need to tell the browser that this HTML document uses
PyScript, and so we create a `
` now looks like:
This fragment of HTML contains the application's header (``), some
instructions between the ` ` tags, an ` ` box for the English text, and
-a `` to click to generate the translation. Towards the end there's a
-`` which will contain the resulting pirate speak as the
-application's output.
-
-There's something strange about the `
` tag: it has a `py-click`
-attribute with the value `translate_english`. This is, in fact, the name of a
-Python function we'll run whenever the button is clicked. Such `py-*` style
-attributes are [built into PyScript](api.md#html-attributes).
+a `` to click to generate the translation. Notice the button has an `id`
+attribute (`id="translate-button"`) which we'll use to attach a Python function
+to its `click` event. Towards the end there's a `` which will
+contain the resulting pirate speak as the application's output.
We put all this together in the `script` tag at the end of the ``. This
-tells the browser we're using PyScript (`type="py"`), and where PyScript
+tells the browser the script is using PyScript (`type="py"`), and where PyScript
should find the Python source code (`src="./main.py"`). Finally, we indicate
where PyScript should find the configuration (`config="./pyscript.json"`).
@@ -175,7 +172,7 @@ In the end, our HTML should look like this:
Polyglot 🦜 💬 🇬🇧 ➡️ 🏴☠️
Translate English into Pirate speak...
-
Translate
+
Translate
@@ -183,22 +180,26 @@ In the end, our HTML should look like this:
```
But this only defines _how_ the user interface should look. To define its
-behaviour we need to write some Python. Specifically, we need to define the
-`translate_english` function, used when the button is clicked.
+behaviour we need to write some Python. Specifically, we need to attach a
+function to the button's click event.
-### main.py
+### `main.py`
The behaviour of the application is defined in `main.py`. It looks like this:
-``` python linenums="1" title="main.py"
+```python linenums="1" title="main.py"
import arrr
-from pyscript import document
+from pyscript import web, when
+@when("click", "#translate-button")
def translate_english(event):
- input_text = document.querySelector("#english")
+ """
+ Translate English text to Pirate speak.
+ """
+ input_text = web.page["english"]
english = input_text.value
- output_div = document.querySelector("#output")
+ output_div = web.page["output"]
output_div.innerText = arrr.translate(english)
```
@@ -207,33 +208,48 @@ It's not very complicated Python code.
On line 1 the `arrr` module is imported so we can do the actual English to
Pirate translation. If we hadn't told PyScript to download the `arrr` module
in our `pyscript.json` configuration file, this line would cause an error.
-PyScript has ensured our environment is set up with the expected `arrr` module.
-
-Line 2 imports the `document` object. The `document` allows us to reach into
-the things on the web page defined in `index.html`.
-
-Finally, on line 5 the `translate_english` function is defined.
-
-The `translate_english` function takes a single parameter called
-`event`. This represents the user's click of the button (but which we don't
-actually use).
-
-Inside the body of the function we first get a reference to the `input`
-element with the [`document.querySelector` function](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
-that takes `#english` as its
-parameter (indicating we want the element with the id "english"). We assign the
-result to `input_text`, then extract the user's `english` from the
-`input_text`'s `value`. Next, we get a reference called `output_div` that
-points to the `div` element with the id "output". Finally, we assign the
-`innerText` of the `output_div` to the result of calling
+PyScript has ensured our environment is set up with the expected `arrr` module
+before our Python code is evaluated.
+
+Line 2 imports the `web` module and the `when` decorator from `pyscript`. The
+`web` module provides a Pythonic way to interact with the web page, while
+`when` is used to easily attach Python functions to browser events.
+
+On line 5 we use the `@when` decorator to attach our function to the button's
+click event. The decorator takes two arguments: the event type (`"click"`) and
+a CSS selector identifying the element (via the id `"#translate-button"`).
+This is PyScript's idiomatic way to handle events - much more Pythonic than
+HTML attributes.
+
+The `translate_english` function is defined on line 6. It takes a single
+parameter called `event`, which represents the browser event that triggered the
+function (in this case, the user's click on the button).
+
+Inside the body of the function we use `web.page["english"]` to get a reference
+to the `
` element with the id "english". The `web.page` object
+represents the current web page, and using the square bracket notation
+(`web.page["element-id"]`) is PyScript's Pythonic way to find elements by their
+unique id. We assign the result to `input_text`, then extract the user's
+`english` text from the `input_text`'s `value` attribute.
+
+Next, we get a reference called `output_div` that points to the `
` element
+with the id "output" using the same `web.page["output"]` pattern. Finally, we
+assign the `innerText` of the `output_div` to the result of calling
[`arrr.translate`](https://arrr.readthedocs.io/en/latest/#arrr.translate)
-(to actually translate the `english` to something piratical).
+to actually translate the `english` to something piratical.
That's it!
+!!! info "Alternative: JavaScript-style DOM access"
+
+ PyScript also provides direct access to the browser's JavaScript APIs. If
+ you're already familiar with JavaScript, you can use `document.querySelector`
+ and other standard
+ [DOM methods](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model).
+
## Editing your app
-If you use an IDE (like VSCode or PyCharm) then you'll probably want them to
+If you use an IDE (like VSCode or PyCharm) then you'll probably want it to
auto-suggest and introspect aspects of the Python code you're writing. The
problem is that the `pyscript` namespace *we provide* isn't installed anywhere
(because it's in your browser, not your IDE's context) so such information
@@ -247,7 +263,7 @@ You should clone the linked-to repository and configure your IDE to consume the
stub files.
For example, let's say you
-[cloned the repository](https://github.com/pyscript/pyscript-stubs) into:
+[cloned the repository](https://github.com/pyscript/pyscript-stubs) into
`~/src/stubs/pyscript-stubs`, then in VSCode, you'd create, in your PyScript
project, a file called `.vscode/settings.json` and add the following:
@@ -257,9 +273,9 @@ project, a file called `.vscode/settings.json` and add the following:
}
```
-Then restart the Python language server in VSCode (Press `Ctrl+Shift+P` (or
-`Cmd+Shift+P` on Mac) to open the Command Palette and type:
-`Python: Restart Language Server`.
+Then restart the Python language server in VSCode (press `Ctrl+Shift+P`, or
+`Cmd+Shift+P` on Mac, to open the Command Palette and type
+`Python: Restart Language Server`).
!!! note
@@ -268,35 +284,28 @@ Then restart the Python language server in VSCode (Press `Ctrl+Shift+P` (or
## Sharing your app
-### PyScript.com
-
-If you're using [pyscript.com](https://pyscript.com), you should save all your files
-and click the "run" button. Assuming you've copied the code properly, you
-should have a fine old time using "Polyglot 🦜" to translate English to
-Pirate-ish.
-
-Alternatively, [click here to see a working example of this app](https://ntoll.pyscriptapps.com/piratical/v5/).
-Notice that the bottom right hand corner contains a link to view the code on
-[pyscript.com](https://pyscript.com). Why not explore the code, copy it to your own
-account and change it to your satisfaction?
-
### From a web server
-Just host the three files (`pyscript.json`, `index.html`
-and `main.py`) in the same directory on a static web server somewhere.
+To share your PyScript application, host the three files (`pyscript.json`,
+`index.html` and `main.py`) in the same directory on a static web server.
-Clearly, we recommend you use [pyscript.com](https://pyscript.com) for this, but any
-static web host will do (for example,
+Any static web host will work (for example,
[GitHub Pages](https://pages.github.com/),
[Amazon's S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html),
[Google Cloud](https://cloud.google.com/storage/docs/hosting-static-website) or
[Microsoft's Azure](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website)).
-## Run PyScript Offline
+### Using PyScript.com
+
+If you're using [pyscript.com](https://pyscript.com) for development, you can also
+deploy directly from there. Save all your files and click the "run" button to test
+your application. The platform provides hosting and generates a shareable link for
+your app.
+
+## Run PyScript offline
To run PyScript offline, without the need of a CDN or internet connection, read
-the [Run PyScript Offline](user-guide/offline.md) section of the user
-guide.
+the [offline guide](user-guide/offline.md) section of the user guide.
We also provide an `offline.zip` file with
[each release](https://pyscript.net/releases/2025.11.2/). This file contains
@@ -308,10 +317,41 @@ could create your offline-first PyScript work.
Congratulations!
-You have just created your first PyScript app. We've explored the core concepts
-needed to build yet more interesting things of your own.
-
-PyScript is extremely powerful, and these beginner steps only just scratch the
-surface. To learn about PyScript in more depth, check out
-[our user guide](user-guide/index.md) or
-[explore our example applications](../examples).
+You have just created your first PyScript app.
+
+But PyScript can do so much more than update text on a page. Here are some of
+the powerful and fun capabilities waiting for you:
+
+**Rich output with `display()`**: Instead of manually finding elements and
+setting their content, you can use PyScript's `display()` function to show
+Python objects, images, and charts directly on your page. Imagine displaying a
+matplotlib chart or a pandas DataFrame with a single function call. Learn more in
+the [user guide](user-guide/display.md).
+
+**Create dynamic interfaces**: The `pyscript.web` module lets you create entire
+user interfaces from Python code - build forms, tables, and interactive
+components without writing HTML. You can compose complex layouts using familiar
+Python syntax. Explore the possibilities in the
+[DOM interaction guide](user-guide/dom.md#pyscriptweb).
+
+**Handle any browser event**: Beyond simple clicks, you can respond to
+keyboard input, mouse movements, form submissions, and more. PyScript makes it
+easy to create rich, interactive experiences. See the
+[events guide](user-guide/events.md) for details.
+
+**Access device capabilities**: Capture photos from the camera, record audio,
+read files from the user's computer, store data locally - PyScript gives your
+Python code access to modern web capabilities. Check out the
+[media guide](user-guide/media.md) and [filesystem guide](user-guide/filesystem.md)
+for more information.
+
+**Build fast, responsive apps**: Use web workers to run Python code in the
+background, keeping your interface smooth even during heavy computation. Perfect
+for data processing, simulations, or any CPU-intensive task.
+[Learn about workers here](user-guide/workers.md).
+
+The [user guide](user-guide/index.md) explores all these topics and more. Keep
+reading to discover what you can build with PyScript! And if you build something
+wonderful, please
+[share it via our community discord server](https://discord.gg/HxvBtukrg2) (we
+love to learn about and celebrate what folks have been up to).
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/game.py b/docs/example-apps/bouncing-ball/game.py
new file mode 100644
index 00000000..d6eb05d9
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/game.py
@@ -0,0 +1,34 @@
+"""
+Bouncing Ball - PyGame-CE demo for PyScript.
+
+Based on the PyGame-CE quickstart tutorial.
+"""
+import asyncio
+import sys
+import pygame
+
+pygame.init()
+
+size = width, height = 320, 240
+speed = [2, 2]
+black = 0, 0, 0
+
+screen = pygame.display.set_mode(size)
+ball = pygame.image.load("intro_ball.gif")
+ballrect = ball.get_rect()
+
+while True:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ sys.exit()
+
+ ballrect = ballrect.move(speed)
+ if ballrect.left < 0 or ballrect.right > width:
+ speed[0] = -speed[0]
+ if ballrect.top < 0 or ballrect.bottom > height:
+ speed[1] = -speed[1]
+
+ screen.fill(black)
+ screen.blit(ball, ballrect)
+ pygame.display.flip()
+ await asyncio.sleep(1/60)
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/index.html b/docs/example-apps/bouncing-ball/index.html
new file mode 100644
index 00000000..ffd34674
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
PyGame-CE Bouncing Ball
+
+
+
+
+
+
Bouncing Ball
+
A simple PyGame-CE demo running in the browser.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/info.md b/docs/example-apps/bouncing-ball/info.md
new file mode 100644
index 00000000..d303d24d
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/info.md
@@ -0,0 +1,40 @@
+# Bouncing Ball
+
+A simple PyGame-CE demonstration running in the browser with PyScript.
+Based on the
+[PyGame-CE quickstart tutorial](https://pyga.me/docs/tutorials/en/intro-to-pygame.html).
+
+## What it shows
+
+- Running PyGame-CE in the browser with the `py-game` script type.
+- Using `await asyncio.sleep()` for frame timing in the browser.
+- Loading game assets through PyScript configuration.
+- Basic game loop with collision detection.
+
+## How it works
+
+The game initialises a pygame display, loads a ball image, and runs an
+infinite game loop. Each frame, it updates the ball position, checks for
+wall collisions (reversing speed on impact), renders the scene, and
+yields control to the browser with `await asyncio.sleep(1/60)`.
+
+The `await` at the top level works because PyScript provides an async
+context. This wouldn't run in standard Python without wrapping in an
+async function.
+
+## Required files
+
+You'll need to download `intro_ball.webp` from the PyGame-CE repository:
+https://raw.githubusercontent.com/pygame-community/pygame-ce/80fe4cb9f89aef96f586f68d269687572e7843f6/docs/reST/tutorials/assets/intro_ball.gif
+
+Place it in the same directory as the other files.
+
+## Running locally
+
+Serve the files with any web server:
+
+```sh
+python -m http.server 8000
+```
+
+Then visit `http://localhost:8000/` in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/bouncing-ball/intro_ball.gif b/docs/example-apps/bouncing-ball/intro_ball.gif
new file mode 100644
index 00000000..bbc4a95f
Binary files /dev/null and b/docs/example-apps/bouncing-ball/intro_ball.gif differ
diff --git a/docs/example-apps/bouncing-ball/pyscript.toml b/docs/example-apps/bouncing-ball/pyscript.toml
new file mode 100644
index 00000000..d226b639
--- /dev/null
+++ b/docs/example-apps/bouncing-ball/pyscript.toml
@@ -0,0 +1,2 @@
+[files]
+"intro_ball.gif" = ""
\ No newline at end of file
diff --git a/docs/example-apps/colour-picker/index.html b/docs/example-apps/colour-picker/index.html
new file mode 100644
index 00000000..ec5b6060
--- /dev/null
+++ b/docs/example-apps/colour-picker/index.html
@@ -0,0 +1,164 @@
+
+
+
+
+
+
Interactive Colour Picker
+
+
+
+
+
+
Interactive Colour Picker
+
+
+
#3498DB
+
+
+
+
+ Red
+ Blue
+ Green
+ Orange
+ Purple
+ Teal
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/colour-picker/info.md b/docs/example-apps/colour-picker/info.md
new file mode 100644
index 00000000..6d1b175d
--- /dev/null
+++ b/docs/example-apps/colour-picker/info.md
@@ -0,0 +1,94 @@
+# Interactive Colour Picker
+
+A colour picker application demonstrating various event handling
+patterns in PyScript.
+
+## What it demonstrates
+
+**Multiple event types:**
+- `input` events - RGB sliders update in real-time.
+- `change` events - Number inputs and hex input.
+- `click` events - Preset buttons and history colours.
+
+**Stacked decorators:**
+- Single function handling multiple sliders with `@when` stacked three
+ times.
+
+**Custom events:**
+- `colour_changed` Event for decoupling colour updates from history
+ management.
+- Shows how to separate concerns in your application.
+
+**Working with form inputs:**
+- Range sliders, number inputs, text inputs.
+- Synchronising values across different input types.
+- Validating and clamping values.
+
+**Dynamic UI updates:**
+- Updating display colour.
+- Maintaining colour history.
+- Creating history elements dynamically.
+
+## Features
+
+- Adjust colours using RGB sliders.
+- Enter RGB values directly with number inputs.
+- Enter hex colour codes.
+- Quick selection from preset colours.
+- Colour history (last 10 colours).
+- Click history to restore colours.
+- Real-time colour display with hex code.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Event handling logic demonstrating various patterns.
+
+## Key patterns demonstrated
+
+### Stacking decorators
+
+```python
+@when("input", "#red-slider")
+@when("input", "#green-slider")
+@when("input", "#blue-slider")
+def handle_slider_change(event):
+ # Single function handles all three sliders.
+ pass
+```
+
+### Custom events for decoupling
+
+```python
+# Define custom event.
+colour_changed = Event()
+
+# Trigger it when colour updates.
+colour_changed.trigger(hex_colour)
+
+# Handle it separately.
+@when(colour_changed)
+def handle_colour_changed(hex_colour):
+ add_to_history(hex_colour)
+```
+
+### Working with form inputs
+
+```python
+@when("input", "#red-slider")
+def handle_slider_change(event):
+ # Get value from slider.
+ value = int(event.target.value)
+ # Update display.
+ update_display(value)
+```
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/colour-picker/main.py b/docs/example-apps/colour-picker/main.py
new file mode 100644
index 00000000..725e77b9
--- /dev/null
+++ b/docs/example-apps/colour-picker/main.py
@@ -0,0 +1,195 @@
+"""
+Interactive Colour Picker - demonstrating event handling in PyScript.
+
+Shows:
+- Multiple event types (input, click, change)
+- Custom events for decoupled logic
+- Working with form inputs
+- Dynamic UI updates
+"""
+from pyscript import when, Event
+from pyscript.web import page
+
+
+# Custom event for when colour changes.
+colour_changed = Event()
+
+# Colour history (limited to 10).
+history = []
+
+
+def rgb_to_hex(r, g, b):
+ """
+ Convert RGB values to hex colour string.
+ """
+ return f"#{r:02X}{g:02X}{b:02X}"
+
+
+def hex_to_rgb(hex_colour):
+ """
+ Convert hex colour string to RGB tuple.
+ """
+ hex_colour = hex_colour.lstrip("#")
+ return tuple(int(hex_colour[i:i+2], 16) for i in (0, 2, 4))
+
+
+def get_current_rgb():
+ """
+ Get current RGB values from sliders.
+ """
+ r = int(page["#red-slider"].value)
+ g = int(page["#green-slider"].value)
+ b = int(page["#blue-slider"].value)
+ return r, g, b
+
+
+def update_display(hex_colour):
+ """
+ Update the colour display with the given hex colour.
+ """
+ display = page["#colour-display"]
+ display.style.backgroundColor = hex_colour
+ display.content = hex_colour
+
+
+def update_controls(r, g, b):
+ """
+ Update all controls to match RGB values.
+ """
+ # Update sliders.
+ page["#red-slider"].value = r
+ page["#green-slider"].value = g
+ page["#blue-slider"].value = b
+
+ # Update number inputs.
+ page["#red-value"].value = r
+ page["#green-value"].value = g
+ page["#blue-value"].value = b
+
+ # Update hex input.
+ hex_colour = rgb_to_hex(r, g, b)
+ page["#hex-input"].value = hex_colour
+
+ # Update display.
+ update_display(hex_colour)
+
+ # Trigger custom event.
+ colour_changed.trigger(hex_colour)
+
+
+def add_to_history(hex_colour):
+ """
+ Add colour to history, maintaining max of 10 items.
+ """
+ if hex_colour in history:
+ return
+
+ history.insert(0, hex_colour)
+ if len(history) > 10:
+ history.pop()
+
+ # Update history display.
+ render_history()
+
+
+def render_history():
+ """
+ Render colour history.
+ """
+ from pyscript.web import div
+
+ container = page["#history-colours"]
+ container.clear()
+
+ for colour in history:
+ colour_div = div(Class="history-colour", title=colour)
+ colour_div.style.backgroundColor = colour
+ colour_div.dataset.colour = colour
+ container.append(colour_div)
+
+
+@when("input", "#red-slider")
+@when("input", "#green-slider")
+@when("input", "#blue-slider")
+def handle_slider_change(event):
+ """
+ Handle RGB slider changes.
+ """
+ r, g, b = get_current_rgb()
+ update_controls(r, g, b)
+
+
+@when("change", "#red-value")
+@when("change", "#green-value")
+@when("change", "#blue-value")
+def handle_number_change(event):
+ """
+ Handle number input changes.
+ """
+ r = int(page["#red-value"].value)
+ g = int(page["#green-value"].value)
+ b = int(page["#blue-value"].value)
+
+ # Clamp values.
+ r = max(0, min(255, r))
+ g = max(0, min(255, g))
+ b = max(0, min(255, b))
+
+ update_controls(r, g, b)
+
+
+@when("change", "#hex-input")
+def handle_hex_change(event):
+ """
+ Handle hex input changes.
+ """
+ hex_colour = event.target.value.strip()
+
+ # Validate hex colour.
+ if not hex_colour.startswith("#"):
+ hex_colour = "#" + hex_colour
+
+ try:
+ r, g, b = hex_to_rgb(hex_colour)
+ update_controls(r, g, b)
+ except (ValueError, IndexError):
+ # Invalid hex colour, ignore.
+ pass
+
+
+@when("click", ".preset-btn")
+def handle_preset_click(event):
+ """
+ Handle preset colour button clicks.
+ """
+ hex_colour = event.target.dataset.colour
+ r, g, b = hex_to_rgb(hex_colour)
+ update_controls(r, g, b)
+
+
+@when("click", ".history-colour")
+def handle_history_click(event):
+ """
+ Handle clicks on history colours.
+ """
+ hex_colour = event.target.dataset.colour
+ r, g, b = hex_to_rgb(hex_colour)
+ update_controls(r, g, b)
+
+
+@when(colour_changed)
+def handle_colour_changed(hex_colour):
+ """
+ Handle custom colour changed event.
+
+ This demonstrates decoupling - the history management doesn't need
+ to know about sliders, presets, or hex inputs.
+ """
+ add_to_history(hex_colour)
+
+
+# Initial setup.
+r, g, b = get_current_rgb()
+hex_colour = rgb_to_hex(r, g, b)
+update_display(hex_colour)
+add_to_history(hex_colour)
\ No newline at end of file
diff --git a/docs/example-apps/display-demo/index.html b/docs/example-apps/display-demo/index.html
new file mode 100644
index 00000000..d2527590
--- /dev/null
+++ b/docs/example-apps/display-demo/index.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
Display Demo
+
+
+
+
+
+
Display Capabilities Demo
+
+
+
+
Basic Types
+
+
Show Basic Types
+
+
+
+
HTML Content
+
+
Show HTML
+
+
+
+
Custom Objects
+
+
Show Custom Object
+
+
+
+
Multiple Values
+
+
Show Multiple
+
+
+
+
Data Cards
+
+
Generate Cards
+
+
+
+
Incremental Updates
+
+
Start Updates
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/display-demo/info.md b/docs/example-apps/display-demo/info.md
new file mode 100644
index 00000000..5b953423
--- /dev/null
+++ b/docs/example-apps/display-demo/info.md
@@ -0,0 +1,98 @@
+# Display Demo
+
+A comprehensive demonstration of PyScript's `display()` function and its
+various capabilities.
+
+## What it demonstrates
+
+**Basic types:**
+- Strings, numbers, booleans, lists, dictionaries.
+- Automatic HTML escaping for safety.
+
+**HTML content:**
+- Using `HTML()` to render unescaped HTML.
+- Creating styled content boxes.
+- Building rich interfaces.
+
+**Custom objects:**
+- Implementing `_repr_html_()` for custom rendering.
+- Creating reusable display components.
+- Table generation with custom styling.
+
+**Multiple values:**
+- Displaying several values in one call.
+- Appending vs. replacing content.
+
+**Incremental updates:**
+- Building UIs progressively.
+- Showing status updates with delays.
+- Creating loading sequences.
+
+## Features
+
+Six interactive panels demonstrating:
+
+1. **Basic Types** - Standard Python objects.
+2. **HTML Content** - Rich formatted content.
+3. **Custom Objects** - Classes with custom display logic.
+4. **Multiple Values** - Batch display operations.
+5. **Data Cards** - Styled metric cards using custom classes.
+6. **Incremental Updates** - Progressive UI building with async.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Display demonstrations with custom classes.
+
+## Key patterns demonstrated
+
+### Custom display representations
+
+```python
+class MetricCard:
+ def __init__(self, label, value, colour):
+ self.label = label
+ self.value = value
+ self.colour = colour
+
+ def _repr_html_(self):
+ return f"
{self.label}: {self.value}
"
+
+# Displays with custom HTML.
+display(MetricCard("Users", "1,234", "#667eea"))
+```
+
+### Multiple format support
+
+```python
+class DataTable:
+ def _repr_html_(self):
+ return "
"
+
+ def __repr__(self):
+ return "Plain text table"
+
+# Automatically uses HTML when available.
+display(table)
+```
+
+### Incremental building
+
+```python
+async def build_ui():
+ display("Step 1", target="output", append=False)
+ await asyncio.sleep(1)
+ display("Step 2", target="output")
+ await asyncio.sleep(1)
+ display("Complete!", target="output")
+```
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/display-demo/main.py b/docs/example-apps/display-demo/main.py
new file mode 100644
index 00000000..c8dd971c
--- /dev/null
+++ b/docs/example-apps/display-demo/main.py
@@ -0,0 +1,205 @@
+"""
+Display Demo - showcasing display() capabilities in PyScript.
+
+Demonstrates:
+- Displaying basic Python types
+- HTML content with the HTML() wrapper
+- Custom objects with _repr_html_()
+- Multiple values at once
+- Building UIs incrementally
+- Replacing vs. appending content
+"""
+from pyscript import display, HTML, when
+import asyncio
+
+
+class MetricCard:
+ """
+ A metric card that displays with custom HTML.
+ """
+ def __init__(self, label, value, colour="#667eea"):
+ self.label = label
+ self.value = value
+ self.colour = colour
+
+ def _repr_html_(self):
+ """
+ Custom HTML representation.
+ """
+ return f"""
+
+
{self.label}
+
{self.value}
+
+ """
+
+ def _darken_colour(self, colour):
+ """
+ Simple colour darkening for gradient.
+ """
+ # Very simple darkening - just for demo purposes.
+ if colour == "#667eea":
+ return "#764ba2"
+ elif colour == "#f093fb":
+ return "#f5576c"
+ elif colour == "#4facfe":
+ return "#00f2fe"
+ return "#333333"
+
+
+class DataTable:
+ """
+ A table with multiple representation formats.
+ """
+ def __init__(self, headers, rows):
+ self.headers = headers
+ self.rows = rows
+
+ def _repr_html_(self):
+ """
+ HTML table representation.
+ """
+ html = "
"
+
+ # Headers.
+ html += ""
+ for header in self.headers:
+ html += f""
+ html += f"{header} "
+ html += " "
+
+ # Rows.
+ for i, row in enumerate(self.rows):
+ bg = "#f8f9fa" if i % 2 == 0 else "white"
+ html += f""
+ for cell in row:
+ html += f"{cell} "
+ html += " "
+
+ html += "
"
+ return html
+
+ def __repr__(self):
+ """
+ Plain text representation.
+ """
+ lines = ["\t".join(self.headers)]
+ lines.extend(["\t".join(str(c) for c in row) for row in self.rows])
+ return "\n".join(lines)
+
+
+@when("click", "#btn-basic")
+def show_basic_types(event):
+ """
+ Display basic Python types.
+ """
+ display("=== Basic Types ===", target="basic-output", append=True)
+ display("String: Hello, World!", target="basic-output", append=True)
+ display(f"Number: {42}", target="basic-output", append=True)
+ display(f"Float: {3.14159}", target="basic-output", append=True)
+ display(f"Boolean: {True}", target="basic-output", append=True)
+ display(f"List: {[1, 2, 3, 4, 5]}", target="basic-output", append=True)
+ display(f"Dict: {{'name': 'Alice', 'age': 30}}", target="basic-output", append=True)
+
+
+@when("click", "#btn-html")
+def show_html_content(event):
+ """
+ Display HTML content.
+ """
+ # Clear and show HTML.
+ display(HTML("
Rich HTML Content "),
+ target="html-output", append=False)
+
+ display(HTML("""
+
This is bold and this is italic .
+ """), target="html-output")
+
+ display(HTML("""
+
+ """), target="html-output")
+
+ display(HTML("""
+
+ Item one
+ Item two
+ Item three
+
+ """), target="html-output")
+
+
+@when("click", "#btn-custom")
+def show_custom_object(event):
+ """
+ Display custom objects with _repr_html_.
+ """
+ display("=== Custom Object ===",
+ target="custom-output", append=False)
+
+ # Display a table.
+ table = DataTable(
+ ["Name", "Score", "Grade"],
+ [
+ ["Alice", 95, "A"],
+ ["Bob", 87, "B"],
+ ["Carol", 92, "A"],
+ ["Dave", 78, "C"]
+ ]
+ )
+ display(table, target="custom-output")
+
+
+@when("click", "#btn-multi")
+def show_multiple_values(event):
+ """
+ Display multiple values at once.
+ """
+ display("First value", "Second value", "Third value",
+ target="multi-output", append=True)
+
+
+@when("click", "#btn-cards")
+def show_metric_cards(event):
+ """
+ Display metric cards.
+ """
+ display(MetricCard("Total Users", "1,234", "#667eea"),
+ target="cards-output", append=False)
+ display(MetricCard("Revenue", "$56,789", "#f093fb"),
+ target="cards-output")
+ display(MetricCard("Growth", "+23%", "#4facfe"),
+ target="cards-output")
+
+
+@when("click", "#btn-incremental")
+async def show_incremental_updates(event):
+ """
+ Build UI incrementally with delays.
+ """
+ display(HTML("
Loading data... "),
+ target="incremental-output", append=False)
+ await asyncio.sleep(0.5)
+
+ display(HTML("
✓ Connected to server
"),
+ target="incremental-output")
+ await asyncio.sleep(0.5)
+
+ display(HTML("
✓ Fetching records
"),
+ target="incremental-output")
+ await asyncio.sleep(0.5)
+
+ display(HTML("
✓ Processing data
"),
+ target="incremental-output")
+ await asyncio.sleep(0.5)
+
+ display(HTML("
Complete! ✓ "),
+ target="incremental-output")
+
+
+# Initial message.
+display(HTML("
Click buttons to see different "
+ "display examples.
"),
+ target="basic-output")
\ No newline at end of file
diff --git a/docs/example-apps/note-taker/index.html b/docs/example-apps/note-taker/index.html
new file mode 100644
index 00000000..460bf45b
--- /dev/null
+++ b/docs/example-apps/note-taker/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
Note Taker
+
+
+
+
+
+
Note Taker
+
Save notes to your local filesystem.
+
+
+ Select Folder
+ Save Note
+
+
+
+
+
Click "Select Folder" to begin...
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/note-taker/info.md b/docs/example-apps/note-taker/info.md
new file mode 100644
index 00000000..7a15ea8f
--- /dev/null
+++ b/docs/example-apps/note-taker/info.md
@@ -0,0 +1,36 @@
+# Note Taker
+
+A simple note-taking application demonstrating local filesystem access.
+
+## What it shows
+
+- Mounting a local directory on user interaction.
+- Writing files to the mounted directory.
+- Syncing changes to persist them locally.
+- Proper error handling.
+
+## How it works
+
+Click "Select Folder" to mount a local directory. The browser will
+prompt you to choose a folder. Once mounted, you can type notes and
+save them to your chosen folder.
+
+The key pattern:
+
+```python
+# Mount (user selects folder).
+await fs.mount("/notes")
+
+# Write files.
+with open("/notes/my-note.txt", "w") as f:
+ f.write(note_text)
+
+# Sync to persist changes.
+await fs.sync("/notes")
+```
+
+## Browser support
+
+This only works in Chromium-based browsers (Chrome, Edge, Brave,
+Vivaldi). Firefox and Safari don't support the File System Access API
+yet.
\ No newline at end of file
diff --git a/docs/example-apps/note-taker/main.py b/docs/example-apps/note-taker/main.py
new file mode 100644
index 00000000..8567c06a
--- /dev/null
+++ b/docs/example-apps/note-taker/main.py
@@ -0,0 +1,45 @@
+"""
+Note Taker - demonstrating local filesystem access.
+"""
+from pyscript import when, fs
+from pyscript.web import page
+
+
+@when("click", "#mount-btn")
+async def mount_folder(event):
+ """
+ Mount a local folder for saving notes.
+ """
+ status = page["#status"]
+ status.content = "Please select a folder..."
+
+ try:
+ await fs.mount("/notes")
+
+ # Enable the UI.
+ page["#save-btn"].disabled = False
+ page["#note"].disabled = False
+ status.content = "Ready! Type your note and click Save."
+ except Exception as e:
+ status.content = f"Error: {e}"
+
+
+@when("click", "#save-btn")
+async def save_note(event):
+ """
+ Save the note to the mounted folder.
+ """
+ status = page["#status"]
+ note_text = page["#note"].value
+
+ try:
+ # Write the note.
+ with open("/notes/my-note.txt", "w") as f:
+ f.write(note_text)
+
+ # Sync to local filesystem.
+ await fs.sync("/notes")
+
+ status.content = "Note saved successfully!"
+ except Exception as e:
+ status.content = f"Error saving: {e}"
\ No newline at end of file
diff --git a/docs/example-apps/overview.md b/docs/example-apps/overview.md
new file mode 100644
index 00000000..a9f2c99b
--- /dev/null
+++ b/docs/example-apps/overview.md
@@ -0,0 +1,3 @@
+# Example Apps
+
+TODO: FINISH THIS!
\ No newline at end of file
diff --git a/docs/example-apps/photobooth/index.html b/docs/example-apps/photobooth/index.html
new file mode 100644
index 00000000..76efdf73
--- /dev/null
+++ b/docs/example-apps/photobooth/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
Photobooth
+
+
+
+
+
+
Photobooth
+
Capture still frames from your webcam.
+
+
+ Start Camera
+ Capture Photo
+
+
+
+
+
Live View
+
+
+
+
Captured Photo
+
+
+
+
+
Click "Start Camera" to begin.
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/photobooth/info.md b/docs/example-apps/photobooth/info.md
new file mode 100644
index 00000000..b274d16b
--- /dev/null
+++ b/docs/example-apps/photobooth/info.md
@@ -0,0 +1,33 @@
+# Photobooth
+
+A simple webcam application demonstrating media device access and still
+frame capture.
+
+## What it shows
+
+- Requesting camera access with `Device.request_stream()`.
+- Displaying live video in a video element.
+- Capturing still frames from video using canvas.
+- Proper error handling for permission denial.
+
+## How it works
+
+Click "Start Camera" to request webcam access. Once granted, the live
+video feed appears. Click "Capture Photo" to grab the current frame and
+display it as a still image on the canvas.
+
+The key technique is using the canvas `drawImage()` method to copy the
+current video frame:
+
+```python
+# Get the canvas context.
+ctx = canvas.getContext("2d")
+
+# Draw the current video frame.
+ctx.drawImage(video, 0, 0, width, height)
+```
+
+## Browser support
+
+This requires a browser with webcam support and HTTPS (or localhost).
+The user must grant camera permission when prompted.
\ No newline at end of file
diff --git a/docs/example-apps/photobooth/main.py b/docs/example-apps/photobooth/main.py
new file mode 100644
index 00000000..0ebf10b8
--- /dev/null
+++ b/docs/example-apps/photobooth/main.py
@@ -0,0 +1,55 @@
+"""
+Photobooth - demonstrating webcam capture and still frame extraction.
+"""
+from pyscript import when
+from pyscript.media import Device
+from pyscript.web import page
+
+
+# Track the current stream.
+current_stream = None
+
+
+@when("click", "#start-btn")
+async def start_camera(event):
+ """
+ Start the camera and display live video.
+ """
+ global current_stream
+
+ status = page["#status"]
+ status.content = "Requesting camera access..."
+
+ try:
+ # Request video stream.
+ current_stream = await Device.request_stream(video=True)
+
+ # Display in video element.
+ video = page["#camera"]
+ video.srcObject = current_stream
+
+ # Update UI.
+ page["#start-btn"].disabled = True
+ page["#capture-btn"].disabled = False
+ status.content = "Camera ready! Click 'Capture Photo' to take a picture."
+ except Exception as e:
+ status.content = f"Error accessing camera: {e}"
+
+
+@when("click", "#capture-btn")
+def capture_photo(event):
+ """
+ Capture a still frame from the video stream.
+ """
+ video = page["#camera"]
+ canvas = page["#photo"]
+
+ # Get the canvas 2D context.
+ ctx = canvas.getContext("2d")
+
+ # Draw the current video frame onto the canvas.
+ ctx.drawImage(video, 0, 0, 400, 300)
+
+ # Update status.
+ status = page["#status"]
+ status.content = "Photo captured!"
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/index.html b/docs/example-apps/pirate-translator/index.html
new file mode 100644
index 00000000..302ecaa9
--- /dev/null
+++ b/docs/example-apps/pirate-translator/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
🦜 Polyglot - Piratical PyScript
+
+
+
+
+
Polyglot 🦜 💬 🇬🇧 ➡️ 🏴☠️
+
Translate English into Pirate speak...
+
+
Translate
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/info.md b/docs/example-apps/pirate-translator/info.md
new file mode 100644
index 00000000..911a6e04
--- /dev/null
+++ b/docs/example-apps/pirate-translator/info.md
@@ -0,0 +1,35 @@
+# Pirate Translator 🦜 💬 🇬🇧 ➡️ 🏴☠️
+
+A simple PyScript application that translates English text into Pirate speak.
+
+## What it demonstrates
+
+- Basic PyScript application structure (HTML, Python, configuration).
+- Using `pyscript.web` to interact with page elements.
+- Event handling with the `@when` decorator.
+- Installing and using third-party Python packages ([arrr](https://arrr.readthedocs.io/en/latest/)).
+
+## Files
+
+- `index.html` - The web page to display.
+- `main.py` - Python code that handles the translation.
+- `pyscript.json` - Configuration specifying required packages.
+
+## How it works
+
+1. User types English text into an input field.
+2. User clicks the "Translate" button.
+3. The `@when` decorator connects the button's click event to `translate_english`.
+4. Function retrieves the input text using `web.page["english"]`.
+5. Text is translated using the `arrr` library.
+6. Result is displayed in the output div using `web.page["output"]`.
+
+## Running locally
+
+Simply serve these files from a web server. For example:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/main.py b/docs/example-apps/pirate-translator/main.py
new file mode 100644
index 00000000..a814718e
--- /dev/null
+++ b/docs/example-apps/pirate-translator/main.py
@@ -0,0 +1,16 @@
+"""
+Pirate translator - translate English to Pirate speak.
+"""
+import arrr
+from pyscript import web, when
+
+
+@when("click", "#translate-button")
+def translate_english(event):
+ """
+ Translate English text to Pirate speak.
+ """
+ input_text = web.page["english"]
+ english = input_text.value
+ output_div = web.page["output"]
+ output_div.innerText = arrr.translate(english)
\ No newline at end of file
diff --git a/docs/example-apps/pirate-translator/pyscript.json b/docs/example-apps/pirate-translator/pyscript.json
new file mode 100644
index 00000000..89e5ce3e
--- /dev/null
+++ b/docs/example-apps/pirate-translator/pyscript.json
@@ -0,0 +1,3 @@
+{
+ "packages": ["arrr"]
+}
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/index.html b/docs/example-apps/prime-worker/index.html
new file mode 100644
index 00000000..259a3cee
--- /dev/null
+++ b/docs/example-apps/prime-worker/index.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+
Prime Numbers with Workers
+
+
+
+
+
+
Prime Number Calculator
+
Main thread stays responsive
+
+
+ Find primes up to:
+
+ Find Primes
+ Stop
+
+
+
+
+
+ Use NumPy (faster)
+
+
+
+
Enter a number and click Find Primes...
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/info.md b/docs/example-apps/prime-worker/info.md
new file mode 100644
index 00000000..0fe8a17f
--- /dev/null
+++ b/docs/example-apps/prime-worker/info.md
@@ -0,0 +1,140 @@
+# Prime Number Calculator with Workers
+
+A demonstration of PyScript workers showing how to keep the main thread
+responsive whilst performing heavy computation in a background worker.
+
+## What it demonstrates
+
+**Worker architecture:**
+- **Main thread**: MicroPython (lightweight, fast startup, responsive UI).
+- **Worker thread**: Pyodide with numpy (heavy computation, numerical
+ libraries).
+- Clear separation of concerns.
+
+**Key patterns:**
+- Starting a worker from the main thread.
+- Calling worker methods with `await`.
+- Sending incremental results back via callbacks.
+- Using `pyscript.sync` to expose functions between threads.
+- Keeping the main thread responsive during computation.
+
+**Visual feedback:**
+- Animated "heartbeat" proves main thread never blocks.
+- Real-time display of primes as they're found.
+- Status updates showing worker progress.
+
+## How it works
+
+### Main thread (MicroPython)
+
+The main thread handles the user interface:
+
+1. Gets reference to the worker via `pyscript.workers`.
+2. Registers a callback function (`handle_prime`) via `pyscript.sync`.
+3. Calls the worker's `find_primes()` method when the button is clicked.
+4. Receives prime numbers via the callback and updates the display.
+5. Stays responsive throughout (watch the pulsing green dot).
+
+### Worker thread (Pyodide)
+
+The worker does the heavy lifting:
+
+1. Exposes `find_primes()` method via `@sync` decorator.
+2. Uses numpy's efficient array operations for the Sieve of Eratosthenes.
+3. Calls back to the main thread's `handle_prime()` for each prime found.
+4. Sends results in batches with small delays to keep UI smooth.
+5. Returns a summary when complete.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Main thread logic (MicroPython).
+- `worker.py` - Worker thread logic (Pyodide with numpy).
+- `worker-config.json` - Worker configuration (numpy package).
+
+## Key code patterns
+
+### Starting the worker
+
+```python
+# Main thread gets reference to worker defined in HTML.
+from pyscript import workers
+
+worker = await workers.py # Name from script tag's type.
+```
+
+### Calling worker methods
+
+```python
+# Main thread calls worker method (must be decorated with @sync).
+result = await worker.find_primes(10000)
+```
+
+### Worker exposing methods
+
+```python
+# Worker exposes method to main thread.
+from pyscript import sync
+
+@sync
+async def find_primes(limit):
+ # Do computation.
+ return result
+```
+
+### Callbacks from worker to main
+
+```python
+# Main thread registers callback.
+from pyscript import sync
+
+async def handle_prime(prime):
+ print(f"Got prime: {prime}")
+
+sync.handle_prime = handle_prime
+
+# Worker calls back to main thread.
+handle_prime = await sync.handle_prime
+await handle_prime(42)
+```
+
+## Why this architecture?
+
+**MicroPython on main thread:**
+- Fast startup (no heavy packages to load).
+- Lightweight (perfect for UI interactions).
+- Stays responsive (no blocking operations).
+
+**Pyodide in worker:**
+- Full Python with scientific libraries (numpy).
+- Heavy computation off the main thread.
+- Can use the full Python ecosystem.
+
+**Best of both worlds:**
+- Fast, responsive UI.
+- Powerful computation when needed.
+- Users never see a frozen interface.
+
+## Try it
+
+1. Enter a number (10 to 100,000).
+2. Click "Find Primes".
+3. Watch the green heartbeat - it never stops pulsing.
+4. See primes appear in real-time.
+
+Try interacting with the page whilst it's computing - everything stays
+smooth because the main thread is never blocked.
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
+
+**Note**: You'll need to serve with appropriate CORS headers for workers
+to access `window` and `document`. See the
+[workers guide](../../user-guide/workers.md#http-headers) for details.
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/main.py b/docs/example-apps/prime-worker/main.py
new file mode 100644
index 00000000..7dc78bd7
--- /dev/null
+++ b/docs/example-apps/prime-worker/main.py
@@ -0,0 +1,89 @@
+"""
+Main thread: MicroPython handling the UI.
+"""
+from pyscript import when, workers
+from pyscript.web import page
+
+
+# Track whether computation is running.
+computing = False
+
+
+@when("click", "#find-btn")
+async def find_primes(event):
+ """
+ Ask the worker to find primes.
+ """
+ global computing
+
+ find_btn = page["#find-btn"]
+ stop_btn = page["#stop-btn"]
+ limit_input = page["#limit"]
+ output = page["#output"]
+
+ # Get and validate the limit.
+ try:
+ limit = int(limit_input.value)
+ if limit < 10 or limit > 1000000:
+ output.content = "Please enter a number between 10 and 1,000,000"
+ return
+ except ValueError:
+ output.content = "Please enter a valid number"
+ return
+
+ # Check if numpy should be used.
+ use_numpy = page["#use-numpy"].checked
+
+ # Update UI state.
+ computing = True
+ find_btn.disabled = True
+ stop_btn.disabled = False
+ limit_input.disabled = True
+ output.content = f"Computing primes up to {limit:,}..."
+
+ try:
+ # Get the worker and call its exported function.
+ worker = await workers["primes"]
+
+ # Time the computation.
+ import time
+ start = time.time()
+ result = await worker.find_primes(limit, use_numpy)
+ elapsed = time.time() - start
+
+ if computing:
+ # Convert to string properly.
+ first_20 = result['first_20']
+ primes_str = ", ".join(str(p) for p in first_20)
+
+ method = "NumPy" if use_numpy else "Pure Python"
+ output.content = f"Found {result['count']:,} primes up to {limit:,}!\n\nMethod: {method}\nTime: {elapsed:.3f} seconds\n\nFirst 20: {primes_str}"
+ except Exception as e:
+ output.content = f"Error: {e}"
+ finally:
+ # Reset UI state.
+ computing = False
+ find_btn.disabled = False
+ stop_btn.disabled = True
+ limit_input.disabled = False
+
+
+@when("click", "#stop-btn")
+def stop_computation(event):
+ """
+ Stop the computation (sets flag, doesn't actually interrupt worker).
+ """
+ global computing
+
+ computing = False
+
+ output = page["#output"]
+ output.content = "Stopped (worker completed, but result discarded)"
+
+ find_btn = page["#find-btn"]
+ stop_btn = page["#stop-btn"]
+ limit_input = page["#limit"]
+
+ find_btn.disabled = False
+ stop_btn.disabled = True
+ limit_input.disabled = False
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/pyscript.json b/docs/example-apps/prime-worker/pyscript.json
new file mode 100644
index 00000000..53070e3d
--- /dev/null
+++ b/docs/example-apps/prime-worker/pyscript.json
@@ -0,0 +1,3 @@
+{
+ "packages": ["numpy"]
+}
\ No newline at end of file
diff --git a/docs/example-apps/prime-worker/worker.py b/docs/example-apps/prime-worker/worker.py
new file mode 100644
index 00000000..f1e5f556
--- /dev/null
+++ b/docs/example-apps/prime-worker/worker.py
@@ -0,0 +1,55 @@
+"""
+Worker thread: Pyodide with numpy doing the computation.
+"""
+import numpy as np
+
+
+def sieve_numpy(limit):
+ """
+ Sieve of Eratosthenes using numpy arrays.
+ """
+ is_prime = np.ones(limit + 1, dtype=bool)
+ is_prime[0] = is_prime[1] = False
+
+ for i in range(2, int(limit**0.5) + 1):
+ if is_prime[i]:
+ is_prime[i*i::i] = False
+
+ primes = np.where(is_prime)[0]
+ return [int(p) for p in primes]
+
+
+def sieve_python(limit):
+ """
+ Sieve of Eratosthenes using pure Python.
+ """
+ is_prime = [True] * (limit + 1)
+ is_prime[0] = is_prime[1] = False
+
+ for i in range(2, int(limit**0.5) + 1):
+ if is_prime[i]:
+ for j in range(i*i, limit + 1, i):
+ is_prime[j] = False
+
+ return [i for i in range(limit + 1) if is_prime[i]]
+
+
+def find_primes(limit, use_numpy=True):
+ """
+ Find all primes up to limit using Sieve of Eratosthenes.
+ """
+ if use_numpy:
+ primes_list = sieve_numpy(limit)
+ else:
+ primes_list = sieve_python(limit)
+
+ first_20 = [int(primes_list[i]) for i in range(min(20, len(primes_list)))]
+
+ return {
+ "count": len(primes_list),
+ "first_20": first_20
+ }
+
+
+# Export functions to make them accessible from main thread.
+__export__ = ["find_primes"]
\ No newline at end of file
diff --git a/docs/example-apps/task-board-ffi/index.html b/docs/example-apps/task-board-ffi/index.html
new file mode 100644
index 00000000..192ec24b
--- /dev/null
+++ b/docs/example-apps/task-board-ffi/index.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+
Task Board - FFI
+
+
+
+
+
+
PyScript Task Board
+
Built with the FFI
+
+
+
+
+ High
+ Medium
+ Low
+
+
Add Task
+
+
+
+ All
+ Active
+ Completed
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/task-board-ffi/info.md b/docs/example-apps/task-board-ffi/info.md
new file mode 100644
index 00000000..e95e24ac
--- /dev/null
+++ b/docs/example-apps/task-board-ffi/info.md
@@ -0,0 +1,84 @@
+# Task Board - FFI Version
+
+The same task management application as the pyscript.web version, but
+implemented using the FFI (foreign function interface) with direct
+JavaScript API calls.
+
+## What it demonstrates
+
+- **Finding elements**: Using `document.getElementById()` and
+ `document.querySelectorAll()`.
+- **Creating elements**: Using `document.createElement()`.
+- **Modifying attributes**: Setting properties like `textContent`,
+ `className`, `checked`.
+- **Working with classes**: Using `classList.add()`,
+ `classList.remove()`.
+- **Collections**: Iterating over NodeLists from `querySelectorAll()`.
+- **Event handling**: Using `@when` decorator with CSS selectors.
+
+## Comparing with pyscript.web
+
+This is the exact same application as the
+[pyscript.web version](../task-board-web/), but implemented using
+JavaScript APIs directly. Key differences:
+
+### Finding elements
+
+**pyscript.web**: `web.page["tasks"]`
+
+**FFI**: `document.getElementById("tasks")`
+
+### Creating elements
+
+**pyscript.web**:
+```python
+task_div = web.div(
+ checkbox,
+ task_text,
+ delete_btn,
+ classes=["task", priority]
+)
+```
+
+**FFI**:
+```python
+task_div = document.createElement("div")
+task_div.className = f"task {priority}"
+task_div.appendChild(checkbox)
+task_div.appendChild(task_text)
+task_div.appendChild(delete_btn)
+```
+
+### Working with classes
+
+**pyscript.web**: `element.classes.add("selected")`
+
+**FFI**: `element.classList.add("selected")`
+
+### Setting content
+
+**pyscript.web**: `element.innerHTML = "text"`
+
+**FFI**: `element.textContent = "text"` or `element.innerHTML = "text"`
+
+## Which approach to use?
+
+Both work perfectly! The pyscript.web version is more Pythonic and
+concise, whilst the FFI version gives you direct access to JavaScript
+APIs. Choose based on your preference and familiarity with web
+development.
+
+## Files
+
+- `index.html` - Page structure and styling (same as web version).
+- `main.py` - Application logic using FFI.
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/task-board-ffi/main.py b/docs/example-apps/task-board-ffi/main.py
new file mode 100644
index 00000000..717f4b9b
--- /dev/null
+++ b/docs/example-apps/task-board-ffi/main.py
@@ -0,0 +1,204 @@
+"""
+Task Board application demonstrating the FFI.
+
+Shows how to use JavaScript APIs directly from Python: querySelector,
+createElement, classList, dataset, and addEventListener. Compare this
+with the pyscript.web version to see the differences.
+"""
+from pyscript import document, when
+
+
+# Track tasks with their DOM elements.
+tasks = []
+current_filter = "all"
+selected_priority = "medium"
+
+
+def update_visibility():
+ """
+ Update visibility of task elements based on current filter.
+ """
+ for task in tasks:
+ if task["deleted"]:
+ task["element"].style.display = "none"
+ continue
+
+ should_show = False
+ if current_filter == "all":
+ should_show = True
+ elif current_filter == "active":
+ should_show = not task["completed"]
+ elif current_filter == "completed":
+ should_show = task["completed"]
+
+ task["element"].style.display = "flex" if should_show else "none"
+
+ # Show empty state if no visible tasks.
+ visible_count = sum(
+ 1 for t in tasks
+ if not t["deleted"] and (
+ current_filter == "all" or
+ (current_filter == "active" and not t["completed"]) or
+ (current_filter == "completed" and t["completed"])
+ )
+ )
+
+ empty_state = document.getElementById("empty-state")
+ if visible_count == 0:
+ if not empty_state:
+ empty = document.createElement("div")
+ empty.id = "empty-state"
+ empty.className = "empty-state"
+ empty.textContent = "No tasks yet. Add one above!"
+ document.getElementById("tasks").appendChild(empty)
+ else:
+ if empty_state:
+ empty_state.remove()
+
+
+def toggle_complete(event):
+ """
+ Toggle task completion status.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["completed"] = event.target.checked
+
+ # Update visual state.
+ task_element = tasks[index]["element"]
+ if event.target.checked:
+ task_element.classList.add("completed")
+ else:
+ task_element.classList.remove("completed")
+
+ # Update visibility based on current filter.
+ update_visibility()
+
+
+def delete_task(event):
+ """
+ Mark a task as deleted.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["deleted"] = True
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("click", "#add-task-btn")
+def add_task(event):
+ """
+ Add a new task when the button is clicked.
+ """
+ task_input = document.getElementById("task-input")
+ text = task_input.value.strip()
+
+ if not text:
+ return
+
+ # Create task object.
+ task_index = len(tasks)
+
+ # Create checkbox.
+ checkbox = document.createElement("input")
+ checkbox.type = "checkbox"
+ checkbox.className = "checkbox"
+ checkbox.checked = False
+ checkbox.dataset.index = str(task_index)
+ checkbox.addEventListener("change", toggle_complete)
+
+ # Create task text.
+ task_text = document.createElement("div")
+ task_text.className = "task-text"
+ task_text.textContent = text
+
+ # Create delete button.
+ delete_btn = document.createElement("button")
+ delete_btn.className = "delete-btn"
+ delete_btn.textContent = "Delete"
+ delete_btn.dataset.index = str(task_index)
+ delete_btn.addEventListener("click", delete_task)
+
+ # Create task container.
+ task_div = document.createElement("div")
+ task_div.className = f"task {selected_priority}"
+ task_div.appendChild(checkbox)
+ task_div.appendChild(task_text)
+ task_div.appendChild(delete_btn)
+
+ # Add to DOM.
+ document.getElementById("tasks").appendChild(task_div)
+
+ # Add task to list.
+ tasks.append({
+ "text": text,
+ "priority": selected_priority,
+ "completed": False,
+ "deleted": False,
+ "element": task_div,
+ "checkbox": checkbox
+ })
+
+ # Clear input.
+ task_input.value = ""
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("keypress", "#task-input")
+def handle_keypress(event):
+ """
+ Add task when Enter is pressed.
+ """
+ if event.key == "Enter":
+ add_task(event)
+
+
+@when("click", ".priority-btn")
+def select_priority(event):
+ """
+ Select a priority level.
+ """
+ global selected_priority
+
+ # Get all priority buttons.
+ priority_btns = document.querySelectorAll(".priority-btn")
+
+ # Remove selected class from all.
+ for btn in priority_btns:
+ btn.classList.remove("selected")
+
+ # Add selected class to clicked button.
+ event.target.classList.add("selected")
+
+ # Update selected priority.
+ selected_priority = event.target.dataset.priority
+
+
+@when("click", ".filter-btn")
+def filter_tasks(event):
+ """
+ Filter tasks by completion status.
+ """
+ global current_filter
+
+ # Get all filter buttons.
+ filter_btns = document.querySelectorAll(".filter-btn")
+
+ # Remove active class from all.
+ for btn in filter_btns:
+ btn.classList.remove("active")
+
+ # Add active class to clicked button.
+ event.target.classList.add("active")
+
+ # Update filter.
+ current_filter = event.target.dataset.filter
+
+ # Update visibility.
+ update_visibility()
+
+
+# Initial setup.
+update_visibility()
\ No newline at end of file
diff --git a/docs/example-apps/task-board-web/index.html b/docs/example-apps/task-board-web/index.html
new file mode 100644
index 00000000..964342a1
--- /dev/null
+++ b/docs/example-apps/task-board-web/index.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+
Task Board - pyscript.web
+
+
+
+
+
+
PyScript Task Board
+
Built with pyscript.web
+
+
+
+
+ High
+ Medium
+ Low
+
+
Add Task
+
+
+
+ All
+ Active
+ Completed
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/example-apps/task-board-web/info.md b/docs/example-apps/task-board-web/info.md
new file mode 100644
index 00000000..1d9b6627
--- /dev/null
+++ b/docs/example-apps/task-board-web/info.md
@@ -0,0 +1,57 @@
+# Task Board - pyscript.web Version
+
+A task management application demonstrating the Pythonic `pyscript.web`
+interface for DOM manipulation.
+
+## What it demonstrates
+
+- **Finding elements**: Using `web.page["id"]` and `web.page.find()`.
+- **Creating elements**: Using `web.div()`, `web.button()`, etc.
+- **Modifying attributes**: Setting `innerHTML`, `value`, `dataset`.
+- **Working with classes**: Using `classes.add()`, `classes.remove()`.
+- **Collections**: Iterating over elements with `.find()`.
+- **Event handling**: Using `@when` decorator with web elements.
+
+## Features
+
+- Add tasks with text descriptions.
+- Set priority levels (high, medium, low) with visual indicators.
+- Mark tasks as complete with checkboxes.
+- Filter tasks by status (all, active, completed).
+- Delete tasks.
+- Visual feedback with colours and styles.
+
+## Files
+
+- `index.html` - Page structure and styling.
+- `main.py` - Application logic using pyscript.web.
+
+## How it works
+
+1. User enters task text and selects a priority level.
+2. Clicking "Add Task" creates a new task object and re-renders.
+3. Tasks are displayed with priority-based colour coding.
+4. Checkboxes toggle completion status.
+5. Filter buttons show different subsets of tasks.
+6. Delete buttons remove tasks from the list.
+
+All DOM manipulation uses `pyscript.web`'s Pythonic interface:
+- Elements accessed via `web.page["id"]` (dictionary-style).
+- Classes managed with set operations (`add`, `remove`).
+- Elements created with function calls (`web.div()`).
+- Events handled with `@when` decorator.
+
+## Compare with FFI version
+
+See the [FFI version](../task-board-ffi/) of this same application to
+compare the Pythonic approach with direct JavaScript API calls.
+
+## Running locally
+
+Serve these files from a web server:
+
+```bash
+python3 -m http.server
+```
+
+Then open http://localhost:8000 in your browser.
\ No newline at end of file
diff --git a/docs/example-apps/task-board-web/main.py b/docs/example-apps/task-board-web/main.py
new file mode 100644
index 00000000..0e9a60b1
--- /dev/null
+++ b/docs/example-apps/task-board-web/main.py
@@ -0,0 +1,210 @@
+"""
+Task Board application demonstrating pyscript.web.
+
+Shows how to find elements, create elements, manipulate attributes,
+work with classes and styles, and handle events using the Pythonic
+pyscript.web interface.
+"""
+from pyscript import when, web
+
+
+# Track tasks with their DOM elements.
+tasks = []
+current_filter = "all"
+selected_priority = "medium"
+
+
+def update_visibility():
+ """
+ Update visibility of task elements based on current filter.
+ """
+ for task in tasks:
+ if task["deleted"]:
+ task["element"].style["display"] = "none"
+ continue
+
+ should_show = False
+ if current_filter == "all":
+ should_show = True
+ elif current_filter == "active":
+ should_show = not task["completed"]
+ elif current_filter == "completed":
+ should_show = task["completed"]
+
+ task["element"].style["display"] = "flex" if should_show else "none"
+
+ # Show empty state if no visible tasks.
+ visible_count = sum(
+ 1 for t in tasks
+ if not t["deleted"] and (
+ current_filter == "all" or
+ (current_filter == "active" and not t["completed"]) or
+ (current_filter == "completed" and t["completed"])
+ )
+ )
+
+ empty_state = web.page["empty-state"]
+ tasks_container = web.page["tasks"]
+
+ if visible_count == 0:
+ if not empty_state:
+ empty = web.div(
+ "No tasks yet. Add one above!",
+ id="empty-state",
+ classes=["empty-state"]
+ )
+ tasks_container.append(empty)
+ else:
+ if empty_state:
+ empty_state._dom_element.remove()
+
+
+def toggle_complete(event):
+ """
+ Toggle task completion status.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["completed"] = event.target.checked
+
+ # Update visual state.
+ task_element = tasks[index]["element"]
+ if event.target.checked:
+ task_element.classes.add("completed")
+ else:
+ task_element.classes.remove("completed")
+
+ # Update visibility based on current filter.
+ update_visibility()
+
+
+def delete_task(event):
+ """
+ Mark a task as deleted.
+ """
+ index = int(event.target.dataset.index)
+ tasks[index]["deleted"] = True
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("click", "#add-task-btn")
+def add_task(event):
+ """
+ Add a new task when the button is clicked.
+ """
+ task_input = web.page["task-input"]
+ text = task_input.value.strip()
+
+ if not text:
+ return
+
+ # Create task object.
+ task_index = len(tasks)
+
+ # Create checkbox.
+ checkbox = web.input(
+ type="checkbox",
+ classes=["checkbox"]
+ )
+ checkbox.dataset.index = str(task_index)
+ checkbox._dom_element.addEventListener("change", toggle_complete)
+
+ # Create task text.
+ task_text = web.div(
+ text,
+ classes=["task-text"]
+ )
+
+ # Create delete button.
+ delete_btn = web.button(
+ "Delete",
+ classes=["delete-btn"]
+ )
+ delete_btn.dataset.index = str(task_index)
+ delete_btn._dom_element.addEventListener("click", delete_task)
+
+ # Create task container.
+ task_div = web.div(
+ checkbox,
+ task_text,
+ delete_btn,
+ classes=["task", selected_priority]
+ )
+
+ # Add to DOM.
+ web.page["tasks"].append(task_div)
+
+ # Add task to list.
+ tasks.append({
+ "text": text,
+ "priority": selected_priority,
+ "completed": False,
+ "deleted": False,
+ "element": task_div,
+ "checkbox": checkbox
+ })
+
+ # Clear input.
+ task_input.value = ""
+
+ # Update visibility.
+ update_visibility()
+
+
+@when("keypress", "#task-input")
+def handle_keypress(event):
+ """
+ Add task when Enter is pressed.
+ """
+ if event.key == "Enter":
+ add_task(event)
+
+
+@when("click", ".priority-btn")
+def select_priority(event):
+ """
+ Select a priority level.
+ """
+ global selected_priority
+
+ # Get all priority buttons.
+ priority_btns = web.page.find(".priority-btn")
+
+ # Remove selected class from all.
+ for btn in priority_btns:
+ btn.classes.remove("selected")
+
+ # Add selected class to clicked button.
+ event.target.classes.add("selected")
+
+ # Update selected priority.
+ selected_priority = event.target.dataset.priority
+
+
+@when("click", ".filter-btn")
+def filter_tasks(event):
+ """
+ Filter tasks by completion status.
+ """
+ global current_filter
+
+ # Get all filter buttons.
+ filter_btns = web.page.find(".filter-btn")
+
+ # Remove active class from all.
+ for btn in filter_btns:
+ btn.classes.remove("active")
+
+ # Add active class to clicked button.
+ event.target.classes.add("active")
+
+ # Update filter.
+ current_filter = event.target.dataset.filter
+
+ # Update visibility.
+ update_visibility()
+
+
+# Initial setup.
+update_visibility()
\ No newline at end of file
diff --git a/docs/faq.md b/docs/faq.md
index bd8a9419..f2a17657 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -1,25 +1,25 @@
-# FAQ
+# Frequently Asked Questions
-This page contains the most common questions and "*gotchas*" asked on
-[our Discord server](https://discord.gg/HxvBtukrg2), in
-[our community calls](https://www.youtube.com/@PyScriptTV), or
-within our community.
+This page addresses common questions and troubleshooting scenarios
+encountered by the PyScript community on
+[Discord](https://discord.gg/HxvBtukrg2), in
+[community calls](https://www.youtube.com/@PyScriptTV), and through
+general usage.
-There are two major areas we'd like to explore:
-[common errors](#common-errors) and [helpful hints](#helpful-hints).
+The FAQ covers two main areas: [common errors](#common-errors) and
+[helpful hints](#helpful-hints).
## Common errors
-### Reading errors
+### Reading error messages
-If your application doesn't run, and you don't see any error messages on the
-page, you should check
+When your application doesn't run and you see no error messages on the
+page, check
[your browser's console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools).
-When reading an error message, the easy way to find out what's going on,
-most of the time, is to read the last line of the error.
+The last line of an error message usually reveals the problem:
-```text title="A Pyodide error."
+```text
Traceback (most recent call last):
File "/lib/python311.zip/_pyodide/_base.py", line 501, in eval_code
.run(globals, locals)
@@ -29,33 +29,28 @@ Traceback (most recent call last):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "
", line 1, in
NameError: name 'failure' is not defined
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
-```text title="A MicroPython error."
+```text
Traceback (most recent call last):
File "", line 1, in
NameError: name 'failure' isn't defined
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
-In both examples, the code created a
+Both examples show a
[`NameError`](https://docs.python.org/3/library/exceptions.html#NameError)
-because the object with the name `failure` did not exist. Everything above the
-error message is potentially useful technical detail.
+because the name `failure` doesn't exist. Everything above the error
+message provides potentially useful technical detail for debugging.
-With this context in mind, these are the most common errors users of PyScript
-encounter.
+These are the most common errors PyScript users encounter.
### SharedArrayBuffer
-This is the first and most common error users may encounter with PyScript:
+This is the most common error new PyScript users face:
!!! failure
- Your application doesn't run and in
- [your browser's console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools)
- you see this message:
+ Your application doesn't run and your browser console shows:
```
Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer
@@ -63,72 +58,64 @@ This is the first and most common error users may encounter with PyScript:
#### When
-This happens when you're unable to access objects in the main thread (`window`
-and `document`) from code running in a web worker.
+This error occurs when code running in a worker tries to access `window`
+or `document` objects that exist on the main thread.
-This error happens because **the server delivering your PyScript application is
-incorrectly configured** or **a `service-worker` attribute has not been used in
-your `script` element**.
+The error indicates either **your web server is incorrectly configured**
+or **a `service-worker` attribute is missing from your script element**.
-Specifically, one of the following three problem situations applies to your
-code:
+Specifically, one of three situations applies:
-* Because of the way your web server is configured, the browser limits the use
- of a technology called "Atomics" (you don't need to know how it works, just
- that it may be limited by the browser). If there is a `worker` attribute in
- your `script` element, and your Python code uses the `window` or `document`
- objects (that actually exist on the main thread), then the browser limitation
- on Atomics will cause the failure, unless you reconfigure your server.
-* There is a `
```
-Alternatively, ensure any JavaScript code you reference uses `export ...` or
-ask for an `.mjs` version of the code. All the various options and technical
-considerations surrounding the use of JavaScript modules in PyScript are
-[covered in our user guide](../user-guide/dom/#working-with-javascript).
+Alternatively, ensure referenced JavaScript code uses `export` or
+request an `.mjs` version. The
+[user guide](../user-guide/dom/#working-with-javascript) covers all
+options and technical considerations for using JavaScript modules.
#### Why
-Even though the standard for JavaScript modules has existed since 2015, many
-old and new libraries still produce files that are incompatible with such
-modern and idiomatic standards.
+Although the JavaScript module standard has existed since 2015, many
+libraries still produce files incompatible with modern standards.
-This isn't so much a technical problem, as a human problem as folks learn to
-use the new standard and migrate old code away from previous and now
-obsolete standards.
+This reflects the JavaScript ecosystem's evolution rather than a
+technical limitation. Developers are learning the new standard and
+migrating legacy code from obsolete patterns.
-While such legacy code exists, be aware that JavaScript code may require
-special care.
+While legacy code exists, JavaScript may require special handling.
### Possible deadlock
-Users may encounter an error message similar to the following:
+This error message indicates a serious problem:
!!! failure
@@ -369,912 +339,601 @@ Users may encounter an error message similar to the following:
#### When
-This error happens when your code on a worker and in the main thread are
-[in a deadlock](https://en.wikipedia.org/wiki/Deadlock). Put simply, neither
-fragment of code can proceed without waiting for the other.
+This error occurs when code on a worker and the main thread are in
+[deadlock](https://en.wikipedia.org/wiki/Deadlock). Neither fragment can
+proceed without waiting for the other.
#### Why
-Let's assume a worker script contains the following Python code:
+Consider this worker code:
-```python title="worker: a deadlock example"
+```python
from pyscript import sync
sync.worker_task = lambda: print('🔥 this is fine 🔥')
-# deadlock 💀🔒
+# Deadlock occurs here. 💀🔒
sync.main_task()
```
-On the main thread, let's instead assume this code:
+And this main thread code:
-```html title="main: a deadlock example"
+```html
```
-When the worker bootstraps and calls `sync.main_task()` on the main thread, it
-blocks until the result of this call is returned. Hence it cannot respond to
-anything at all. However, in the code on the main thread, the
-`sync.worker_task()` in the worker is called, but the worker is blocked! Now
-the code on both the main thread and worker are mutually blocked and waiting
-on each other. We are in a classic
-[deadlock](https://en.wikipedia.org/wiki/Deadlock) situation.
-
-The moral of the story? Don't create such circular deadlocks!
+The main thread calls `main_task()`, which awaits `worker_task()` on the
+worker. But `worker_task()` can only execute after `main_task()`
+completes. Neither can proceed - classic deadlock.
-How?
+PyScript detects this situation and raises the error to prevent your
+application from freezing.
-The mutually blocking calls cause the deadlock, so simply don't block.
-
-For example, on the main thread, let's instead assume this code:
-
-```html title="main: avoiding deadlocks"
-
-```
+#### Solution
-By scheduling the call to the worker (rather than awaiting it), it's possible
-for the main thread to call functions defined in the worker in a non-blocking
-manner, thus allowing the worker to also work in an unblocked manner and react
-to such calls. We have resolved the mutual deadlock.
+Restructure your code to avoid circular dependencies between main thread
+and worker. One thread should complete its work before the other begins,
+or they should work independently without waiting for each other.
### TypeError: crypto.randomUUID is not a function
-If PyScript fails to start and you look in the browser console, you may
-find the following error:
+This error appears in specific browser environments:
!!! failure
```
- main.js:43 Uncaught TypeError: crypto.randomUUID is not a function
- at main.js:43:26
+ TypeError: crypto.randomUUID is not a function
```
#### When
-This happens because PyScript uses the `crypto.randomUUID` function, and the
-web page isn't served correctly.
+This occurs when using PyScript in environments where the
+`crypto.randomUUID` API isn't available. This typically happens in:
-#### Why
+Older browsers not supporting this API.
-This error is _created by the browser_ because `crypto.randomUUID` requires a
-secure context or localhost to use the latest web standards that are part of
-PyScript's core (such as `crypto.randomUUID`).
+Non-secure contexts (HTTP instead of HTTPS). The `crypto.randomUUID`
+function requires a secure context.
-Put simply, your code should be served from a domain secured
-with TLS (i.e. the domain name starts with `https` - use a service like
-[let's encrypt](https://letsencrypt.org/) to address this) or from `localhost`
-if developing and viewing your site on your development machine.
+Certain embedded browser environments or WebViews with restricted APIs.
-This is something PyScript can't fix. Rather, it's how the web works and you
-should always ensure your code is served in a secure manner.
+#### Solution
-## Helpful hints
+Use HTTPS for your application. The `crypto` API requires secure
+contexts.
-This section contains common hacks or hints to make using PyScript easier.
+Update to modern browsers supporting the full Web Crypto API.
-!!! Note
+If working in a restricted environment, you may need to polyfill
+`crypto.randomUUID` or use an alternative approach for generating unique
+identifiers.
- We have an absolutely lovely PyScript contributor called
- [Jeff Glass](https://github.com/jeffersglass) who maintains an exceptional
- blog full of [PyScript recipes](https://pyscript.recipes/) with even more
- use cases, hints, tips and solutions. Jeff also has a
- [wonderful YouTube channel](https://www.youtube.com/@CodingGlass) full of
- very engaging PyScript related content.
-
- If you cannot find what you are looking for here, please check Jeff's blog
- as it's likely he's probably covered something close to the situation in
- which you find yourself.
+## Helpful hints
- Of course, if ever you meet Jeff in person, please buy him a beer and
- remember to say a big "thank you". 🍻
+This section provides guidance on common scenarios and best practices.
### PyScript `latest`
-PyScript follows the [CalVer](https://calver.org/) convention for version
-numbering.
-
-Put simply, it means each version is numbered according to when, in the
-calendar, it was released. For instance, version `2024.4.2` was the _second_
-release in the month of April in the year 2024 (**not** the release on the 2nd
-of April but the second release **in** April).
-
-It used to be possible to reference PyScript via a version called `latest`,
-which would guarantee you always got the latest release.
-
-However, at the end of 2023, we decided to **stop supporting `latest` as a
-way to reference PyScript**. We did this for two broad reasons:
-
-1. In the autumn of 2023, we release a completely updated version of PyScript
- with some breaking changes. Folks who wrote for the old version, yet still
- referenced `latest`, found their applications broke. We want to avoid this
- at all costs.
-2. Our release cadence is more regular, with around two or three releases a
- month. Having transitioned to the new version of PyScript, we aim to avoid
- breaking changes. However, we are refining and adding features as we adapt
- to our users' invaluable feedback.
-
-Therefore,
-[pinning your app's version of PyScript to a specific release](https://github.com/pyscript/pyscript/releases)
-(rather than `latest`) ensures you get exactly the version of PyScript you
-used when writing your code.
-
-However, as we continue to develop PyScript it _is_ possible to get our latest
-development version of PyScript via `npm` and we could (should there be enough
-interest) deliver our work-in-progress via a CDN's "canary" or "development"
-channel. **We do not guarantee the stability of such versions of PyScript**,
-so never use them in production, and our documentation may not reflect the
-development version.
-
-If you require the development version of PyScript, these are the URLs to use:
-
-```html title="PyScript development. ⚠️⚠️⚠️ WARNING: HANDLE WITH CARE! ⚠️⚠️⚠️"
-
-
+When including PyScript in your HTML, you can reference specific
+versions or use `latest`:
+
+```html
+
+
+
+
+
```
-!!! warning
+#### Production vs development
+
+For production applications, always use specific version numbers. This
+ensures your application continues working even when new PyScript
+versions are released. Updates happen on your schedule, not
+automatically.
+
+For development and experimentation, `latest` provides convenient access
+to new features without updating version numbers.
- ***Do not use shorter urls or other CDNs.***
+#### Version compatibility
- PyScript needs both the correct headers to use workers and to find its own
- assets at runtime. Other CDN links might result into a **broken
- experience**.
+When reporting bugs or asking questions, always mention which PyScript
+version you're using. Different versions may behave differently, and
+version information helps diagnose problems.
-### Workers via JavaScript
+Check the [releases page](https://pyscript.net/releases/) to see
+available versions and their release notes.
-Sometimes you want to start a Pyodide or MicroPython web worker from
-JavaScript.
+### Workers via JavaScript
-Here's how:
+You can create workers programmatically from JavaScript:
-```html title="Starting a PyScript worker from JavaScript."
+```html
```
-```python title="micro.py"
-from pyscript import sync
+This approach is useful when:
-def do_stuff():
- print("heavy computation")
+You're building primarily JavaScript applications that need Python
+functionality.
-# Note: this reference is awaited in the JavaScript code.
-sync.doStuff = do_stuff
-```
+You want dynamic worker creation based on runtime conditions.
+
+You're integrating PyScript into existing JavaScript frameworks.
+
+The worker runs Python code in a separate thread, keeping your main
+thread responsive. Use `worker.sync` to call Python functions from
+JavaScript, and vice versa through `pyscript.window`.
### JavaScript `Class.new()`
-When using Python to instantiate a class defined in JavaScript, one needs to
-use the class's `new()` method, rather than just using `Class()` (as in
-Python).
-
-Why?
-
-The reason is technical, related to JavaScript's history and its relatively
-poor introspection capabilities:
-
-* In JavaScript, `typeof function () {}` and `typeof class {}` produce the
- same outcome: `function`. This makes it **very hard to disambiguate the
- intent of the caller** as both are valid, JavaScript used to use
- `function` (rather than `class`) to instantiate objects, and the class you're
- using may not use the modern, `class` based, idiom.
-* In the FFI, the JavaScript proxy has traps to intercept the use of the
- `apply` and `construct` methods used during instantiation. However, because
- of the previous point, it's not possible to be sure that `apply` is meant to
- `construct` an instance or call a function.
-* Unlike Python, just invoking a `Class()` in JavaScript (without
- [the `new` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new))
- throws an error.
-* Using `new Class()` is invalid syntax in Python. So there is still a need to
- somehow disambiguate the intent to call a function or instantiate a class.
-* Making use of the capitalized-name-for-classes convention is brittle because
- when JavaScript code is minified the class name can sometimes change.
-* This leaves our convention of `Class.new()` to explicitly signal the intent
- to instantiate a JavaScript class. While not ideal it is clear and
- unambiguous.
+When creating JavaScript class instances from Python, use the `.new()`
+method:
-### PyScript events
+```python
+from pyscript import window
-PyScript uses hooks during the lifecycle of the application to facilitate the
-[creation of plugins](../user-guide/plugins/).
+# Create a new Date instance.
+date = window.Date.new()
-Beside hooks, PyScript also dispatches events at specific moments in the
-lifecycle of the app, so users can react to changes in state:
+# Create other class instances.
+map_instance = window.Map.new()
+set_instance = window.Set.new()
+```
-#### m/py:ready
+This pattern exists because Python's `Date()` would attempt to call the
+JavaScript class as a function rather than constructing an instance.
-Both the `mpy:ready` and `py:ready` events are dispatched for every PyScript
-related element found on the page. This includes `
-
-bootstrapping
-
```
-A classic use case for this event is to recreate the "starting up"
-spinner that used to be displayed when PyScript bootstrapped. Just show the
-spinner first, then close it once `py:ready` is triggered!
+#### Available events
-!!! warning
+**`py:ready`** - Pyodide interpreter is ready and about to run code.
- If using Pyodide on the main thread, the UI will block until Pyodide has
- finished bootstrapping. The "starting up" spinner won't work unless Pyodide
- is started on a worker instead.
-
-#### m/py:done
-
-The `mpy:done` and `py:done` events dispatch after the either the synchronous
-or asynchronous code has finished execution.
-
-```html title="A py:done example."
-
+**`mpy:ready`** - MicroPython interpreter is ready and about to run
+code.
-
-bootstrapping
-
-```
+**`py:done`** - All Pyodide scripts have finished executing.
-!!! warning
+**`mpy:done`** - All MicroPython scripts have finished executing.
- If `async` code contains an infinite loop or some orchestration that keeps
- it running forever, then these events may never trigger because the code
- never really finishes.
+**`py:all-done`** - All PyScript activity has completed.
-#### py:all-done
+#### Event details
-The `py:all-done` event dispatches when all code is finished executing.
+Events carry useful information in their `detail` property:
-This event is special because it depends upon all the MicroPython and Pyodide
-scripts found on the page, no matter the interpreter.
+```javascript
+document.addEventListener('py:ready', (event) => {
+ // Access the script element.
+ const script = event.detail.script;
+
+ // Access the interpreter wrapper.
+ const wrap = event.detail.wrap;
+});
+```
-In this example, MicroPython waves before Pyodide before the `"everything is
-done"` message is written to the browser's console.
+Use these events to:
-```html title="A py:all-done example."
-
-
-
-```
+Show loading indicators whilst Python initialises.
-#### m/py:progress
+Coordinate between JavaScript and Python code.
-The `py:progress` or `mpy:progress` event triggers on the main thread *during*
-interpreter bootstrap (no matter if your code is running on main or in a
-worker).
+Enable UI elements only after Python is ready.
-The received `event.detail` is a string that indicates operations between
-`Loading {what}` and `Loaded {what}`. So, the first event would be, for
-example, `Loading Pyodide` and the last one per each bootstrap would be
-`Loaded Pyodide`.
+Track application lifecycle for debugging or analytics.
-In between all operations are `event.detail`s, such as:
+### Packaging pointers
- * `Loading files` and `Loaded files`, when `[files]` is found in the optional
- config
- * `Loading fetch` and `Loaded fetch`, when `[fetch]` is found in the optional
- config
- * `Loading JS modules` and `Loaded JS modules`, when `[js_modules.main]` or
- `[js_modules.worker]` is found in the optional config
- * finally, all optional packages handled via *micropip* or *mip* will also
- trigger various `Loading ...` and `Loaded ...` events so that users can see
- what is going on while PyScript is bootstrapping
+Understanding packaging helps you use external Python libraries
+effectively.
-An example of this listener applied to a dialog can be [found in here](https://agiammarchi.pyscriptapps.com/kmeans-in-panel-copy/v1/).
+#### Pyodide packages
-### Packaging pointers
+Pyodide includes many pre-built packages. Check the
+[package list](https://pyodide.org/en/stable/usage/packages-in-pyodide.html)
+to see what's available.
+
+Install pure Python packages from PyPI using `micropip`:
+
+```python
+import micropip
-Applications need third party packages and [PyScript can be configured to
-automatically install packages for you](user-guide/configuration/#packages).
-Yet [packaging can be a complicated beast](#python-packages), so here are some
-hints for a painless packaging experience with PyScript.
-
-There are essentially five ways in which a third party package can become
-available in PyScript.
-
-1. The module is already part of either the Pyodide or MicroPython
- distribution. For instance, Pyodide includes numpy, pandas, scipy,
- matplotlib and scikit-learn as pre-built packages you need only activate
- via the [`packages` setting](../user-guide/configuration/#packages) in
- PyScript. There are plans for MicroPython to offer different builds for
- PyScript, some to include MicroPython's version of numpy or the API for
- sqlite.
-2. Host a standard Python package somewhere (such as
- [PyScript.com](https://pyscript.com) or in a GitHub repository) so it can
- be fetched as a package via a URL at runtime.
-3. Reference hosted Python source files, to be included on the file
- system, via the [`files` setting](../user-guide/configuration/#files).
-4. Create a folder containing the package's files and sub folders, and create
- a hosted `.zip` or `.tgz`/`.tar.gz`/`.whl` archive to be decompressed into
- the file system (again, via the
- [`files` setting](../user-guide/configuration/#files)).
-5. Provide your own `.whl` package and reference it via a URL in the
- `packages = [...]` list.
-
-#### Host a package
-
-Just put the package you need somewhere it can be served (like
-[PyScript.com](https://pyscript.com/)) and reference the URL in the
-[`packages` setting](../user-guide/configuration/#packages). So long as the
-server at which you are hosting the package
-[allows CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
-(fetching files from other domains) everything should just work.
-
-It is even possible to install such packages at runtime, as this example using
-MicroPython's [`mip` tool](https://docs.micropython.org/en/latest/reference/packages.html)
-demonstrates (the equivalent can be achieved with Pyodide
-[via `micropip`](https://micropip.pyodide.org/en/stable/)).
-
-```python title="MicroPython mip example."
-# Install default version from micropython-lib
-mip.install("keyword")
-
-# Install from raw URL
-mip.install("https://raw.githubusercontent.com/micropython/micropython-lib/master/python-stdlib/bisect/bisect.py")
-
-# Install from GitHub shortcut
-mip.install("github:jeffersglass/some-project/foo.py")
+await micropip.install("pillow")
```
-#### Provide your own file
+Some packages with C extensions are available. If `micropip` reports it
+cannot find a pure Python wheel, the package either:
-One can use the [`files` setting](../user-guide/configuration/#files) to copy
-packages onto the Python path:
+Contains C extensions not compiled for WebAssembly.
-```html title="A file copied into the Python path."
-
-[files]
-"./modules/bisect.py" = "./bisect.py"
-
-
-```
+Isn't compatible with the browser environment.
-#### Code archive (`zip`/`tgz`/`whl`)
+Has dependencies that aren't available.
-Compress all the code you want into an archive (using either either `zip` or
-`tgz`/`tar.gz`). Host the resulting archive and use the
-[`files` setting](../user-guide/configuration/#files) to decompress it onto
-the Python interpreter's file system.
+#### MicroPython packages
-Consider the following file structure:
+MicroPython uses packages from
+[micropython-lib](https://github.com/micropython/micropython-lib).
+Reference them in configuration:
-```
-my_module/__init__.py
-my_module/util.py
-my_module/sub/sub_util.py
+```toml
+[packages]
+"unittest" = ""
```
-Host it somewhere, and decompress it into the home directory of the Python
-interpreter:
+For packages not in micropython-lib, use the `files` configuration to
+include them:
-```html title="A code archive."
-
+```toml
[files]
-"./my_module.zip" = "./*"
-
+"my_package.py" = "https://example.com/my_package.py"
+```
-
+Or reference local files:
+
+```toml
+[files]
+"my_package.py" = "./my_package.py"
```
-Please note, the target folder must end with a star (`*`), and will contain
-everything in the archive. For example, `"./*"` refers to the home folder for
-the interpreter.
+#### Package size considerations
+
+Pyodide packages can be large. The `numpy` package alone is several
+megabytes. Consider:
-### File System
+Using MicroPython for applications where package access isn't critical.
-Python expects a file system. In PyScript each interpreter provides its own
-in-memory **virtual** file system. **This is not the same as the filesystem
-on the user's device**, but is simply a block of memory in the browser.
+Loading only necessary packages.
-!!! warning
+Showing loading indicators whilst packages download.
- **The file system is not persistent nor shareable** (yet).
+Caching packages for offline use in production applications.
- Every time a user loads or stores files, it is done in ephemeral memory
- associated with the current browser session. Beyond the life of the
- session, nothing is shared, nothing is stored, nothing persists!
+### Filesystem
-#### Read/Write
+PyScript provides virtual filesystems through Emscripten. Understanding
+how they work helps you manage files effectively.
-The easiest way to add content to the virtual file system is by using native
-Python file operations:
+#### Virtual filesystem basics
-```python title="Writing to a text file."
-with open("./test.txt", "w") as dest:
- dest.write("hello vFS")
- dest.close()
+Both Pyodide and MicroPython run in sandboxed environments with virtual
+filesystems. These aren't the user's actual filesystem - they're
+in-memory or browser-storage-backed filesystems provided by Emscripten.
-# Read and print the written content.
-with open("./test.txt", "r") as f:
- content = f.read()
- print(content)
+Files you create or modify exist only in this virtual environment. They
+persist during the session but may not survive page reloads unless
+explicitly saved to browser storage.
+
+#### Loading files
+
+Use the `files` configuration to make files available:
+
+```toml
+[files]
+"data.csv" = "./data.csv"
+"config.json" = "https://example.com/config.json"
```
-Combined with our `pyscript.fetch` utility, it's also possible to store more
-complex data from the web.
+PyScript downloads these files and places them in the virtual
+filesystem. Your Python code can then open them normally:
-```python title="Writing a binary file."
-# Assume async execution.
-from pyscript import fetch, window
+```python
+with open("data.csv") as f:
+ data = f.read()
+```
-href = window.location.href
+#### Writing files
-with open("./page.html", "wb") as dest:
- dest.write(await fetch(href).bytearray())
+You can create and write files in the virtual filesystem:
-# Read and print the current HTML page.
-with open("./page.html", "r") as source:
- print(source.read())
+```python
+with open("output.txt", "w") as f:
+ f.write("Hello, world!")
```
-#### Upload
+These files exist in memory. To provide them for download, use the
+browser's download mechanism:
-It's possible to upload a file onto the virtual file system from the browser
-(` `), and using the DOM API.
+```python
+from pyscript import window, ffi
-The following fragment is just one way to achieve this. It's very simple and
-builds on the file system examples already seen.
-```html title="Upload files onto the virtual file system via the browser."
-
-
+def download_file(filename, content):
+ """
+ Trigger browser download of file content.
+ """
+ blob = window.Blob.new([content], ffi.to_js({"type": "text/plain"}))
+ url = window.URL.createObjectURL(blob)
+
+ link = window.document.createElement("a")
+ link.href = url
+ link.download = filename
+ link.click()
+
+ window.URL.revokeObjectURL(url)
-
-
+
+# Use it.
+download_file("output.txt", "File contents here")
```
-#### Download
+#### Browser storage
-It is also possible to create a temporary link through which you can download
-files present on the interpreter's virtual file system.
+For persistent storage across sessions, use browser storage APIs:
+```python
+from pyscript import window
-```python title="Download file from the virtual file system."
-from pyscript import document, ffi, window
-import os
+# Save data to localStorage.
+window.localStorage.setItem("key", "value")
-def download_file(path, mime_type):
- name = os.path.basename(path)
- with open(path, "rb") as source:
- data = source.read()
+# Retrieve data.
+value = window.localStorage.getItem("key")
+```
+
+Or use the File System Access API for actual file access (requires user
+permission):
- # Populate the buffer.
- buffer = window.Uint8Array.new(len(data))
- for pos, b in enumerate(data):
- buffer[pos] = b
- details = ffi.to_js({"type": mime_type})
+```python
+from pyscript import window
- # This is JS specific
- file = window.File.new([buffer], name, details)
- tmp = window.URL.createObjectURL(file)
- dest = document.createElement("a")
- dest.setAttribute("download", name)
- dest.setAttribute("href", tmp)
- dest.click()
- # here a timeout to window.URL.revokeObjectURL(tmp)
- # should keep the memory clear for the session
+# Request file picker (modern browsers only).
+file_handle = await window.showSaveFilePicker()
+writable = await file_handle.createWritable()
+await writable.write("content")
+await writable.close()
```
### create_proxy
-The `create_proxy` function is described in great detail
-[on the FFI page](../user-guide/ffi/), but it's also useful to explain _when_
-`create_proxy` is needed and the subtle differences between Pyodide and
-MicroPython.
-
-#### Background
+The `create_proxy` function manages Python-JavaScript reference
+lifecycles.
-To call a Python function from JavaScript, the native Python function needs
-to be wrapped in a JavaScript object that JavaScript can use. This JavaScript
-object converts and normalises arguments passed into the function before
-handing off to the native Python function. It also reverses this process with
-any results from the Python function, and so converts and normalises values
-before returning the result to JavaScript.
+#### When to use create_proxy
-The JavaScript primitive used for this purpose is the
-[Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
-It enables "traps", such as
-[apply](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply),
-so the extra work required to call the Python function can happen.
+In Pyodide on the main thread, wrap Python functions passed as
+JavaScript callbacks:
-Once the `apply(target, self, args)` trap is invoked:
+```python
+from pyscript import ffi, window
-* JavaScript must find the correct Python interpreter to evaluate the code.
-* In JavaScript, the `self` argument for `apply` is probably ignored for most
- common cases.
-* All the `args` must be resolved and converted into their Python primitive
- representations or associated Python objects.
-Ultimately, the targets referenced in the `apply` **must exist** in the Python
-context so they are ready when the JavaScript `apply` method calls into the
-Python context.
+def callback(event):
+ """
+ Handle events.
+ """
+ print(event.type)
-**Here's the important caveat**: locally scoped Python functions, or functions
-created at run time cannot be retained forever.
-```python title="A basic Python to JavaScript callback."
-import js
-
-js.addEventListener(
- "custom:event",
- lambda e: print(e.type)
-)
+# Create proxy before passing to JavaScript.
+window.addEventListener("click", ffi.create_proxy(callback))
```
-In this example, the anonymous `lambda` function has no reference in the Python
-context. It's just delegated to the JavaScript runtime via `addEventListener`,
-and then Python immediately garbage collects it. However, as previously
-mentioned, such a Python object must exist for when the `custom:event` is
-dispatched.
+#### When create_proxy isn't needed
-Furthermore, there is no way to define how long the `lambda` should be kept
-alive in the Python environment, nor any way to discover if the `custom:event`
-callback will ever dispatch (so the `lambda` is forever pending). PyScript, the
-browser and the Python interpreters can only work within a finite amount of
-memory, so memory management and the "aliveness" of objects is important.
+In workers, PyScript automatically manages references. You don't need
+`create_proxy`:
-Therefore, `create_proxy` is provided to delegate responsibility for the
-lifecycle of an object to the author of the code. In other words, wrapping the
-`lambda` in a call to `create_proxy` would ensure the Python interpreter
-retains a reference to the anonymous function for future use.
-
-!!! info
+```python
+from pyscript import window
- This probably feels strange! An implementation detail of how the Python
- and JavaScript worlds interact with each other is bleeding into your code
- via `create_proxy`. Surely, if we always just need to create a proxy, a
- more elegant solution would be to do this automatically?
- As you'll see, this is a complicated situation with inevitable tradeoffs,
- but ultimately, through the
- [`experimental_create_proxy = "auto"` flag](../user-guide/configuration/#experimental_create_proxy),
- you probably never need to use `create_proxy`. This section of
- our docs gives you the context you need to make an informed decision.
+def callback(event):
+ """
+ Handle events in worker.
+ """
+ print(event.type)
-**However**, this isn't the end of the story.
-When a Python callback is attached to a specific JavaScript
-instance (rather than passed as argument into an event listener), it is easy
-for the Python interpreter to know when the function could be freed from the
-memory.
+# No create_proxy needed in workers.
+window.addEventListener("click", callback)
+```
-```python title="A sticky lambda."
-from pyscript import document
+With `experimental_create_proxy = "auto"` in configuration, PyScript
+automatically wraps functions:
-# logs "click" if nothing else stopped propagation
-document.onclick = lambda e: print(e.type)
+```toml
+[experimental_create_proxy]
+auto = true
```
-"**Wait, wat? This doesn't make sense at all!?!?**", is a valid
-question/response to this situation.
+```python
+from pyscript import window
-In this case there's
-no need to use `create_proxy` because the JavaScript reference to which the
-function is attached isn't going away and the interpreter can use the
-[`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)
-to destroy the `lambda` (or decrease its reference count) when the underlying
-JavaScript reference to which it is attached is itself destroyed.
-#### In Pyodide
+def callback(event):
+ """
+ Handle events with auto proxying.
+ """
+ print(event.type)
-The `create_proxy` utility was created
-([among others](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#module-pyodide.ffi.wrappers))
-to smooth out and circumvent the afore mentioned memory issues when using
-Python callables with JavaScript event handlers.
-Using it requires special care. The coder must invoke the `destroy()` method
-when the Python callback is no longer needed. It means coders must track the
-callback's lifecycle. But this is not always possible:
+# No create_proxy needed with auto mode.
+window.addEventListener("click", callback)
+```
-* If the callback is passed into opaque third party libraries, the reference is
- "lost in a limbo" where who-knows-when the reference should be freed.
-* If the callback is passed to listeners, timers or promises it's hard to
- predict when the callback is no longer needed.
+#### In MicroPython
-Luckily the `Promise` use case is automatically handled by Pyodide, but we're
-still left with the other cases:
+MicroPython creates proxies automatically. The `create_proxy` function
+exists for code portability between Pyodide and MicroPython, but it's
+just a pass-through in MicroPython:
-```python title="Different Pyodide create_proxy contexts."
+```python
from pyscript import ffi, window
-# The create_proxy is needed when a Python
-# function isn't attached to an object reference
-# (but is, rather, an argument passed into
-# the JavaScript context).
-
-# This is needed so a proxy is created for
-# future use, even if `print` won't ever need
-# to be freed from the Python runtime.
-window.setTimeout(
- ffi.create_proxy(print),
- 100,
- "print"
-)
-
-# This is needed because the lambda is
-# immediately garbage collected.
-window.setTimeout(
- ffi.create_proxy(
- lambda x: print(x)
- ),
- 100,
- "lambda"
-)
-def print_type(event):
+def callback(event):
+ """
+ Handle events.
+ """
print(event.type)
-# This is needed even if `print_type`
-# is not a scoped / local function.
-window.addEventListener(
- "some:event",
- ffi.create_proxy(print_type),
- # despite this intent, the proxy
- # will be trapped forever if not destroyed
- ffi.to_js({"once": True})
-)
-# This does NOT need create_function as it is
-# attached to an object reference, hence observed to free.
-window.Object().no_create_function = lambda: print("ok")
+# Works with or without create_proxy in MicroPython.
+window.addEventListener("click", callback)
+window.addEventListener("click", ffi.create_proxy(callback))
```
-To simplify this complicated situation PyScript has an
-`experimental_create_proxy = "auto"` flag. When set, **PyScript intercepts
-JavaScript callback invocations, such as those in the example code above, and
-automatically proxies and destroys any references that are garbage collected
-in the JavaScript environment**.
+Both versions work identically in MicroPython.
-**When this flag is set to `auto` in your configuration, you should never need
-to use `create_proxy` with Pyodide**.
+#### Manual proxy destruction
-!!! Note
+If manually managing proxies in Pyodide, destroy them when done:
- When it comes code running on a web worker, due to the way browser work, no
- Proxy can survive a round trip to the main thread and back.
+```python
+from pyscript import ffi, window
- In this scenario PyScript works differently and references callbacks
- via a unique id, rather than by their identity on the worker. When running
- on a web worker, PyScript automatically frees proxy object references, so
- you never need to use `create_proxy` when running code on a web worker.
-#### In MicroPython
+def callback(event):
+ """
+ One-time handler.
+ """
+ print(event.type)
-The proxy situation is definitely simpler in MicroPython. It just creates
-proxies automatically (so there is no need for a manual `create_proxy` step).
-This is because MicroPython doesn't (yet) have a `destroy()` method for
-proxies, rendering the use case of `create_proxy` redundant.
+proxy = ffi.create_proxy(callback)
+window.addEventListener("click", proxy, ffi.to_js({"once": True}))
-Accordingly, **the use of `create_proxy` in MicroPython is only needed for
-code portability purposes** between Pyodide and MicroPython. When using
-`create_proxy` in MicroPython, it's just a pass-through function and doesn't
-actually do anything.
+# After the event fires once, destroy the proxy.
+# (In practice, the "once" option auto-removes it, but this shows the
+# pattern for cases where you manage lifecycle manually.)
+proxy.destroy()
+```
-All the examples that require `create_proxy` in Pyodide, don't need it in
-MicroPython:
+Manual destruction prevents memory leaks when callbacks are no longer
+needed.
-```python title="Different MicroPython create_proxy contexts."
-from pyscript import window
+### to_js
-# This just works.
-window.setTimeout(print, 100, "print")
+The `to_js` function converts Python objects to JavaScript equivalents.
-# This also just works.
-window.setTimeout(lambda x: print(x), 100, "lambda")
+#### Python dicts to JavaScript objects
-def print_type(event):
- print(event.type)
+Python dictionaries convert to JavaScript object literals, not Maps:
-# This just works too.
-window.addEventListener(
- "some:event",
- print_type,
- ffi.to_js({"once": True})
-)
+```python
+from pyscript import ffi, window
+
+
+config = {"async": False, "cache": True}
+
+# Converts to JavaScript object literal.
+js_config = ffi.to_js(config)
-# And so does this.
-window.Object().no_create_function = lambda: print("ok")
+# Pass to JavaScript APIs expecting objects.
+window.someAPI(js_config)
```
-### to_js
+This differs from Pyodide's default behaviour (which creates Maps).
+PyScript's `to_js` always creates object literals for better JavaScript
+compatibility.
-Use of the `pyodide.ffi.to_js` function is described
-[in the ffi page](../user-guide/ffi/#to_js).
-But it's also useful to cover the *when* and *why* `to_js` is needed, if at
-all.
+#### When to use to_js
-#### Background
+Use `to_js` when passing Python data structures to JavaScript APIs:
-Despite their apparent similarity,
-[Python dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries)
-and
-[JavaScript object literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects)
-are very different primitives:
+```python
+from pyscript import ffi, window
-```python title="A Python dictionary."
-ref = {"some": "thing"}
-# Keys don't need quoting, but only when initialising a dict...
-ref = dict(some="thing")
-```
+# Passing configuration objects.
+options = {"method": "POST", "headers": {"Content-Type": "application/json"}}
+window.fetch("/api", ffi.to_js(options))
-```js title="A JavaScript object literal."
-const ref = {"some": "thing"};
+# Passing arrays.
+numbers = [1, 2, 3, 4, 5]
+window.console.log(ffi.to_js(numbers))
-// Keys don't need quoting, so this is as equally valid...
-const ref = {some: "thing"};
+# Passing nested structures.
+data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
+window.processData(ffi.to_js(data))
```
-In both worlds, accessing `ref["some"]` would produce the same result: the
-string `"thing"`.
-
-However, in JavaScript `ref.some` (i.e. a dotted reference to the key) would
-also work to return the string `"thing"` (this is not the case in Python),
-while in Python `ref.get("some")` achieves the same result (and this is not the
-case in JavaScript).
-
-Perhaps because of this, Pyodide chose to convert Python dictionaries to
-JavaScript
-[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
-objects that share a
-[`.get` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get)
-with Python.
-
-Unfortunately, in idiomatic JavaScript and for the vast majority of APIs,
-an object literal (rather than a `Map`) is used to represent key/value pairs.
-Feedback from our users indicates the dissonance of using a `Map` rather than
-the expected object literal to represent a Python `dict` is the source of a
-huge amount of frustration. Sadly, the APIs for `Map` and object literals
-are sufficiently different that one cannot be a drop in replacement for
-another.
-
-Pyodide have provided a way to override the default `Map` based behaviour, but
-this results some rather esoteric code:
-
-```python title="Convert a dict to an object literal in Pyodide."
-import js
-from pyodide.ffi import to_js
-
-js.callback(
- to_js(
- {"async": False},
- # Transform the default Map into an object literal.
- dict_converter=js.Object.fromEntries
- )
-)
-```
-!!! info
+#### Important caveat
- Thanks to a
- [recent change in Pyodide](https://github.com/pyodide/pyodide/pull/4576),
- such `Map` instances are
- [duck-typed](https://en.wikipedia.org/wiki/Duck_typing) to behave like
- object literals. Conversion may not be needed anymore, and `to_js` may just
- work without the need of the `dict_converter`. Please check.
+!!! warning
-MicroPython's version of `to_js` takes the opposite approach (for
-many of the reasons stated above) and converts Python dictionaries to object
-literals instead of `Map` objects.
+ Objects created by `to_js` are detached from the original Python
+ object. Changes to the JavaScript object don't affect the Python
+ object:
+
+ ```python
+ from pyscript import ffi, window
+
+ python_dict = {"key": "value"}
+ js_object = ffi.to_js(python_dict)
+
+ # Modify JavaScript object.
+ js_object.key = "new value"
+
+ # Python dict unchanged.
+ print(python_dict["key"]) # Still "value"
+ ```
-As a result, **the PyScript `pyscript.ffi.to_js` ALWAYS returns a JavaScript
-object literal by default when converting a Python dictionary** no matter if
-you're using Pyodide or MicroPython as your interpreter. Furthermore, when
-using MicroPython, because things are closer to idiomatic JavaScript behaviour,
-you may not even need to use `to_js` unless you want to ensure
-cross-interpreter compatibility.
+This detachment is usually desirable - you're passing data to
+JavaScript, not sharing mutable state. But be aware of this behaviour.
-#### Caveat
+#### MicroPython differences
-!!! warning
+MicroPython's `to_js` already creates object literals by default. You
+may not need `to_js` in MicroPython unless ensuring cross-interpreter
+compatibility:
+
+```python
+from pyscript import window
- **When using `pyscript.to_js`, the result is detached from the original
- Python dictionary.**
+# Works in MicroPython without to_js.
+config = {"async": False}
+window.someAPI(config)
-Any change to the JavaScript object **will not be reflected in the original
-Python object**. For the vast majority of use cases, this is a desirable
-trade-off. But it's important to note this detachment.
+# But using to_js ensures Pyodide compatibility.
+from pyscript import ffi
+window.someAPI(ffi.to_js(config))
+```
-If you're simply passing data around, `pyscript.ffi.to_js` will fulfil your
-requirements in a simple and idiomatic manner.
+For code that might run with either interpreter, use `to_js`
+consistently.
\ No newline at end of file
diff --git a/docs/user-guide/architecture.md b/docs/user-guide/architecture.md
index be311b6e..4fb4fdff 100644
--- a/docs/user-guide/architecture.md
+++ b/docs/user-guide/architecture.md
@@ -1,301 +1,202 @@
-# Architecture, Lifecycle & Interpreters
-
-## Core concepts
-
-PyScript's architecture has three core concepts:
-
-1. A small, efficient and powerful kernel called
- [PolyScript](https://github.com/pyscript/polyscript) is the foundation
- upon which PyScript and plugins are built.
-2. A library called [coincident](https://github.com/WebReflection/coincident#readme)
- that simplifies and coordinates interactions with web workers.
-3. The PyScript [stack](https://en.wikipedia.org/wiki/Solution_stack) inside
- the browser is simple and clearly defined.
-
-### PolyScript
-
-[PolyScript](https://github.com/pyscript/polyscript) is the core of PyScript.
-
-!!! danger
-
- Unless you are an advanced user, you only need to know that PolyScript
- exists, and it can be safely ignored.
-
-PolyScript's purpose is to bootstrap the platform and provide all the necessary
-core capabilities. Setting aside PyScript for a moment, to use
-*just PolyScript* requires a `
-
-
-
-
-
-