diff --git a/docs/api.md b/docs/api.md
index e5dceb3..869535f 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -331,6 +331,86 @@ 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).
diff --git a/docs/beginning-pyscript.md b/docs/beginning-pyscript.md
index d153bdf..38da6ac 100644
--- a/docs/beginning-pyscript.md
+++ b/docs/beginning-pyscript.md
@@ -117,8 +117,8 @@ module in the document's `
` tag:
🦜 Polyglot - Piratical PyScript
-
-
+
+
@@ -168,8 +168,8 @@ In the end, our HTML should look like this:
🦜 Polyglot - Piratical PyScript
-
-
+
+
Polyglot 🦜 💬 🇬🇧 ➡️ 🏴☠️
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index f036330..7766403 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -121,9 +121,11 @@ version of Pyodide as specified in the previous examples:
### Files
-The `files` option fetches arbitrary content from URLs onto the filesystem
-available to Python, and emulated by the browser. Just map a valid URL to a
-destination filesystem path.
+The `files` option fetches arbitrary content from URLs onto the virtual
+filesystem available to Python, and emulated by the browser. Just map a valid
+URL to a destination filesystem path on the in-browser virtual filesystem. You
+can find out more in the section about
+[PyScript and filesystems](../filesystem/).
The following JSON and TOML are equivalent:
diff --git a/docs/user-guide/filesystem.md b/docs/user-guide/filesystem.md
new file mode 100644
index 0000000..a7c824b
--- /dev/null
+++ b/docs/user-guide/filesystem.md
@@ -0,0 +1,175 @@
+# PyScript and Filesystems
+
+As you know, the filesystem is where you store files. For Python to work there
+needs to be a filesystem in which Python packages, modules and data for your
+apps can be found. When you `import` a library, or when you `open` a file, it
+is on the in-browser virtual filesystem that Python looks.
+
+However, things are not as they may seem.
+
+This section clarifies what PyScript means by a filesystem, and the way in
+which PyScript interacts with such a concept.
+
+## Two filesystems
+
+PyScript interacts with two filesystems.
+
+1. The browser, thanks to
+ [Emscripten](https://emscripten.org/docs/api_reference/Filesystem-API.html),
+ provides a virtual in-memory filesystem. **This has nothing to do with your
+ device's local filesystem**, but is contained within the browser based
+ sandbox used by PyScript. The [files](../configuration/#files)
+ configuration API defines what is found on this filesystem.
+2. PyScript provides an easy to use API for accessing your device's local
+ filesystem. It requires permission from the user to mount a folder from the
+ local filesystem onto a directory in the browser's virtual filesystem. Think
+ of it as gate-keeping a bridge to the outside world of the device's local
+ filesystem.
+
+!!! danger
+
+ Access to the device's local filesystem **is only available in Chromium
+ based browsers**.
+
+ Firefox and Safari do not support this capability (yet), and so it is not
+ available to PyScript running in these browsers.
+
+## The in-browser filesystem
+
+The filesystem that both Pyodide and MicroPython use by default is the
+[in-browser virtual filesystem](https://emscripten.org/docs/api_reference/Filesystem-API.html).
+Opening files and importing modules takes place in relation to this sandboxed
+environment, configured via the [files](../configuration/#files) entry in your
+settings.
+
+```toml title="Filesystem configuration via TOML."
+[files]
+"https://example.com/myfile.txt": ""
+```
+
+```python title="Just use the resulting file 'as usual'."
+# Interacting with the virtual filesystem, "as usual".
+with open("myfile.txt", "r") as myfile:
+ print(myfile.read())
+```
+
+Currently, each time you re-load the page, the filesystem is recreated afresh,
+so any data stored by PyScript to this filesystem will be lost.
+
+!!! info
+
+ In the future, we may make it possible to configure the in-browser virtual
+ filesystem as persistent across re-loads.
+
+[This article](https://emscripten.org/docs/porting/files/file_systems_overview.html)
+gives an excellent overview of the browser based virtual filesystem's
+implementation and architecture.
+
+The most important key concepts to remember are:
+
+* The PyScript filesystem is contained *within* the browser's sandbox.
+* Each instance of a Python interpreter used by PyScript runs in a separate
+ sandbox, and so does NOT share virtual filesystems.
+* All Python related filesytem operations work as expected with this
+ filesystem.
+* The virtual filesystem is configured via the
+ [files](../configuration/#files) entry in your settings.
+* The virtual filesystem is (currently) NOT persistent between page re-loads.
+* Currently, the filesystem has a maximum capacity of 4GB of data (something
+ over which we have no control).
+
+## The device's local filesystem
+
+**Access to the device's local filesystem currently only works on Chromium
+based browsers**.
+
+Your device (the laptop, mobile or tablet) that runs your browser has a
+filesystem provided by a hard drive. Thanks to the
+[`pyscript.fs` namespace in our API](../../api/#pyscriptfs), both MicroPython
+and Pyodide (CPython) gain access to this filesystem should the user of
+your code allow this to happen.
+
+This is a [transient activation](https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation)
+for the purposes of
+[user activation of gated features](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation).
+Put simply, before your code gains access to their local filesystem, an
+explicit agreement needs to be gathered from the user. Part of this process
+involves asking the user to select a target directory on their local
+filesystem, to which PyScript will be given access.
+
+The directory on their local filesystem, selected by the user, is then mounted
+to a given directory inside the browser's virtual filesystem. In this way a
+mapping is made between the sandboxed world of the browser, and the outside
+world of the user's filesystem.
+
+Your code will then be able to perform all the usual filesystem related
+operations provided by Python, within the mounted directory. However, **such
+changes will NOT take effect on the local filesystem UNTIL your code
+explicitly calls the `sync` function**. At this point, the state of the
+in-browser virtual filesystem and the user's local filesystem are synchronised.
+
+The following code demonstrates the simplest use case:
+
+```python title="The core operations of the pyscript.fs API"
+from pyscript import fs
+
+# Ask once for permission to mount any local folder
+# into the virtual filesystem handled by Pyodide/MicroPython.
+# The folder "/local" refers to the directory on the virtual
+# filesystem to which the user-selected directory will be
+# mounted.
+await fs.mount("/local")
+
+# ... DO FILE RELATED OPERATIONS HERE ...
+
+# If changes were made, ensure these are persisted to the local filesystem's
+# folder.
+await fs.sync("/local")
+
+# If needed to free RAM or that specific path, sync and unmount
+await fs.unmount("/local")
+```
+
+It is possible to use multiple different local directories with the same mount
+point. This is important if your application provides some generic
+functionality on data that might be in different local directories because
+while the nature of the data might be similar, the subject is not. For
+instance, you may have different models for a PyScript based LLM in different
+directories, and may wish to switch between them at runtime using different
+handlers (requiring their own transient action). In which case use
+the following technique:
+
+```python title="Multiple local directories on the same mount point"
+# Mount a local folder specifying a different handler.
+# This requires a user explicit transient action (once).
+await fs.mount("/local", id="v1")
+# ... operate on that folder ...
+await fs.unmount("/local")
+
+# Mount a local folder specifying a different handler.
+# This also requires a user explicit transient action (once).
+await fs.mount("/local", id="v2")
+# ... operate on that folder ...
+await fs.unmount("/local")
+
+# Go back to the original handler or a previous one.
+# No transient action required now.
+await fs.mount("/local", id="v1")
+# ... operate again on that folder ...
+```
+
+In addition to the mount `path` and handler `id`, the `fs.mount` function can
+take two further arguments:
+
+* `mode` (by default `"readwrite"`) indicates the sort of activity available to
+ the user. It can also be set to `read` for read-only access to the local
+ filesystem. This is a part of the
+ [web-standards](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#mode)
+ for directory selection.
+* `root` - (by default, `""`) is 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).
+
+The `sync` and `unmount` functions only accept the mount `path` used in the
+browser's local filesystem.
diff --git a/docs/user-guide/first-steps.md b/docs/user-guide/first-steps.md
index 7fcd843..abe37f8 100644
--- a/docs/user-guide/first-steps.md
+++ b/docs/user-guide/first-steps.md
@@ -20,9 +20,9 @@ CSS:
-
+
-
+
diff --git a/docs/user-guide/plugins.md b/docs/user-guide/plugins.md
index baf094e..7aed1cb 100644
--- a/docs/user-guide/plugins.md
+++ b/docs/user-guide/plugins.md
@@ -100,7 +100,7 @@ For example, this will work because all references are contained within the
registered function:
```js
-import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js";
+import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js";
hooks.worker.onReady.add(() => {
// NOT suggested, just an example!
@@ -114,7 +114,7 @@ hooks.worker.onReady.add(() => {
However, due to the outer reference to the variable `i`, this will fail:
```js
-import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js";
+import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js";
// NO NO NO NO NO! ☠️
let i = 0;
@@ -147,7 +147,7 @@ the page.
```js title="log.js - a plugin that simply logs to the console."
// import the hooks from PyScript first...
-import { hooks } from "https://pyscript.net/releases/2025.2.2/core.js";
+import { hooks } from "https://pyscript.net/releases/2025.2.3/core.js";
// The `hooks.main` attribute defines plugins that run on the main thread.
hooks.main.onReady.add((wrap, element) => {
@@ -197,8 +197,8 @@ hooks.worker.onAfterRun.add(() => {
-
-
+
+
+
PyWorker - mpy bootstrapping pyodide example
diff --git a/mkdocs.yml b/mkdocs.yml
index 44eb83a..3757deb 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -72,6 +72,7 @@ nav:
- The DOM & JavaScript: user-guide/dom.md
- Web Workers: user-guide/workers.md
- The FFI in detail: user-guide/ffi.md
+ - PyScript and filesystems: user-guide/filesystem.md
- Python terminal: user-guide/terminal.md
- Python editor: user-guide/editor.md
- PyGame-CE: user-guide/pygame-ce.md
diff --git a/version.json b/version.json
index d05966c..142d482 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
{
- "version": "2025.2.2"
+ "version": "2025.2.3"
}