Serverless Streamlit Running Entirely in Your Browser
Streamlit is a Python web app framework for the fast development of data apps. This project is to make it run completely on web browsers with the power of Pyodide, WebAssembly (Wasm)-ported Python.
Visit Stlite Sharing.
See @stlite/desktop
.
You can use Stlite on your web page loading the script and CSS files via <script>
and <link>
tags as below.
Here is a sample HTML file.
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>Stlite App</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.73.0/build/stlite.css"
/>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.73.0/build/stlite.js"></script>
<script>
stlite.mount(
`
import streamlit as st
name = st.text_input('Your name')
st.write("Hello,", name or "world")
`,
document.getElementById("root"),
);
</script>
</body>
</html>
In this sample,
- Stlite library is imported with the first script tag, then the global
stlite
object becomes available. stlite.mount()
mounts the Streamlit app on the<div id="root" />
element as specified via the second argument. The app script is passed via the first argument.
⚠️ If you are using backticks`
inside your app script (e.g. if you have included markdown sections with code highlighting) they would close the script block inst.mount(` ... `)
. To avoid this, you can escape them with with a preceding backslash\
.<script> stlite.mount( ` import streamlit as st st.markdown("This is an inline code format: \`code\`") `, document.getElementById("root"), ); </script>
If more controls are needed such as installing dependencies or mounting multiple files, use the following API instead.
stlite.mount(
{
requirements: ["matplotlib"], // Packages to install
entrypoint: "streamlit_app.py", // The target file of the `streamlit run` command
files: {
"streamlit_app.py": `
import streamlit as st
import matplotlib.pyplot as plt
import numpy as np
size = st.slider("Sample size", 100, 1000)
arr = np.random.normal(1, 1, size=size)
fig, ax = plt.subplots()
ax.hist(arr, bins=20)
st.pyplot(fig)
`,
},
streamlitConfig: {
// Streamlit configuration
"client.toolbarMode": "viewer",
},
},
document.getElementById("root"),
);
You can pass an object to the files
option to mount files onto the file system, whose keys are file paths, and you can specify the values in various ways as below. See also the File system section for more details.
You can pass the file content as a string or binary data.
This is what we did in the example above.
stlite.mount(
{
files: {
"path/to/text_file.txt": "file content",
"path/to/binary_file.bin": new Uint8Array([0x00, 0x01, 0x02, 0x03]),
},
// ... other options ...
},
document.getElementById("root"),
);
You can use this way to load a file from a URL and mount it to the specified path on the virtual file system.
Either an absolute or relative URL is accepted. Consider as the same as the url
option of the fetch()
function.
stlite.mount(
{
files: {
"path/to/file": {
url: "https://example.com/path/to/file",
},
"path/to/file2": {
url: "./path/to/file",
},
},
// ... other options ...
},
document.getElementById("root"),
);
Stlite runs on Pyodide, and it has a file system provided by Emscripten.
The files specified via the files
option are mounted on the file system, and Emscripten's FS.writeFile()
function is used internally for it.
You can specify the options (opts
) for the FS.writeFile(path, data, opts)
function as below.
stlite.mount(
{
files: {
"path/to/text_file.txt": {
data: "file content",
opts: {
encoding: "utf8",
},
},
"path/to/file": {
url: "https://example.com/path/to/file",
opts: {
encoding: "utf8",
},
},
},
// ... other options ...
},
document.getElementById("root"),
);
You can load archive files such as zip files, unpack them, and mount the unpacked files to the file system by using the archives
option.
The url
field of each item accepts either an absolute or relative URL. Consider as the same as the url
option of the fetch()
function.
The downloaded archive file is unpacked by the pyodide.unpackArchive(buffer, format, options)
function. You have to pass the rest of the arguments of the function, format
and options
as below.
mount(
{
archives: [
{
url: "./foo.zip",
// buffer: new Uint8Array([...archive file binary...]), // You can also pass the binary data directly
format: "zip",
options: {},
},
],
// ... other options ...
},
document.getElementById("root"),
);
You can pass the multiple files to the files
option as below to construct the multipage app structure, the entry point file and pages/*.py
files.
Read the Streamlit official document about the multipage apps.
stlite.mount(
{
entrypoint: "👋_Hello.py",
files: {
"👋_Hello.py": `
import streamlit as st
st.set_page_config(page_title="Hello")
st.title("Main page")
`,
"pages/1_⭐️_Page1.py": `
import streamlit as st
st.set_page_config(page_title="Page1")
st.title("Page 1")
`,
"pages/2_🎈_Page2.py": `
import streamlit as st
st.set_page_config(page_title="Page2")
st.title("Page 2")
`,
},
},
document.getElementById("root"),
);
You can pass the Streamlit configuration options to the streamlitConfig
field as key-value pairs as below. Unlike the original Streamlit configuration, the options are passed as a flat object with the keys separated by dots.
stlite.mount(
{
streamlitConfig: {
"theme.base": "dark",
"theme.primaryColor": "#00b4d8",
"theme.backgroundColor": "#03045e",
"theme.secondaryBackgroundColor": "#0077b6",
"theme.textColor": "#caf0f8",
"client.toolbarMode": "viewer",
"client.showErrorDetails": false,
},
// ... other options ...
},
document.getElementById("root"),
);
In the example above, the Stlite script is loaded via the <script>
tag with the versioned URL.
You can use another version by changing the version number in the URL.
The following URLs are also available, while our recommendation is to use the versioned one as above because the API may change without backward compatibility in future releases.
<script src="https://cdn.jsdelivr.net/npm/@stlite/mountable/build/stlite.js"></script>
You can use the latest version of the published Stlite package with this URL.
<script src="https://whitphx.github.io/stlite/lib/mountable/stlite.js"></script>
This URL points to the head of the main branch which is usually ahead of the released packages. However, we strongly recommend NOT to use this URL because this might be broken and there is no guarantee that this resource will be kept available in the future.
Stlite uses Pyodide and loads it from the CDN by default. You can use your own Pyodide distribution by passing the URL to the pyodideUrl
option as below. This would be helpful for example when your organization has a restrictive policy for CDN access.
stlite.mount(
{
pyodideUrl: "https://<your-pyodide-distribution-url>/pyodide.js",
// ... other options ...
},
document.getElementById("root"),
);
Pyodide provides two distribution types, full and core, and you should serve the full distribution in this case.
Stlite loads some packages from the Pyodide distribution such as micropip
and they are not included in the core distribution.
Even with the full distribution whose size is quite large (+200MB), only the necessary packages are loaded on demand, so the actual size of loaded resources is smaller and you don't have to choose the core distribution worrying about the size. Ref: #1007.
Stlite executes your Python code on Pyodide in the browser, and Pyodide has its own Linux-like file system isolated from the host OS (see Pyodide's or Emscripten's documents about the FS for details).
The source code and data files are mounted on the file system through the files
and archives
options as described above, and the Python code can access them. So, for example, what open("path/to/file")
reads or writes is the file on the file system virtually existing in the browser, not a file on the host OS.
The default file system (MEMFS
) is ephemeral, so the files saved in the directories are lost when the page is reloaded.
The root /
and some directories including home are mounted as MEMFS
, the ephemeral file system, by default.
To persist the files across the app restarts, you can use the IndexedDB-based file system (IDBFS
). The files saved in the directories mounted with IDBFS
are stored in the browser's IndexedDB, so they are persistent across the app restarts.
In the case of @stlite/mountable
, you can mount the IndexedDB-based file system, IDBFS
to the specified directories in the virtual file system, by passing the idbfsMountpoints
option as below.
The mounted file system is persistent across the page reloads and the browser sessions.
stlite.mount(
{
idbfsMountpoints: ["/mnt"], // Mount the IndexedDB-based file system to the /mnt directory.
entrypoint: "streamlit_app.py",
files: {
"streamlit_app.py": `
import datetime
import streamlit as st
with open("/mnt/data.txt", "a") as f:
f.write(f"{datetime.datetime.now()}\\n")
with open("/mnt/data.txt", "r") as f:
st.code(f.read())
`,
},
// ... other options ...
},
document.getElementById("root"),
);
To make HTTP requests, these libraries work on Stlite.
requests
(only these classes and methods)urllib
(only these classes and methods)urllib3
(since 2.2.0)pyodide.http.pyfetch()
andpyodide.http.open_url()
- See also the following section about top-level await to know how to use the async method
pyodide.http.pyfetch()
.
- See also the following section about top-level await to know how to use the async method
Stlite automatically enables koenvo/pyodide-http
's patches to make requests
and urllib
work,
while the networking libraries do not work in general on the Pyodide runtime (Python in browser) as written in this doc and Pyodide provides its standard alternative methods to make HTTP requests, pyodide.http.pyfetch()
and pyodide.http.open_url()
.
Also, urllib3
supports Pyodide since 2.2.0 as this document says.
As Stlite runs on the web browser environment (Pyodide runtime), there are things not working well. The known issues follow.
-
st.spinner()
does not work with blocking methods likepyodide.http.open_url()
because Stlite runs on a single-threaded environment, sost.spinner()
can't execute its code to start showing the spinner during the blocking method occupies the only event loop.- If you want to show a spinner with a blocking method, add a 0.1-second sleep before the blocking method call, although this will definitely add an empty 0.1-second wait to the execution.
with st.spinner("Running a blocking method..."): await asyncio.sleep(0.1) # Add this line to wait for the spinner to start showing some_blocking_method()
- If you want to show a spinner with a blocking method, add a 0.1-second sleep before the blocking method call, although this will definitely add an empty 0.1-second wait to the execution.
-
st.bokeh_chart()
does not work since Pyodide uses Bokeh version 3.x while Streamlit only supports 2.x. The 3.x support for Streamlit is tracked here: streamlit/streamlit#5858 -
time.sleep()
is no-op. Useasyncio.sleep()
instead. This is a restriction from Pyodide runtime. See pyodide/pyodide#2354. The following section about top-level await may also help to know how to use async functions on Stlite. -
st.write_stream()
should be used with an async generator function rather than a normal generator function. Due to the same reason asst.spinner()
above, the normal generator function does not work well in the browser environment, while it still can be passed tost.write_stream()
. The following is an example ofst.write_stream()
with an async generator function.async def stream(): for i in range(10): yield i await asyncio.sleep(1) st.write_stream(stream)
-
There are some small differences in how (less common) data types of DataFrame columns are handled in
st.dataframe()
,st.data_editor()
,st.table()
, and Altair-based charts. The reason is that Stlite uses the Parquet format instead of the Arrow IPC format to serialize dataframes (Ref: #601). -
Packages including binary extensions (e.g. C/Rust/Fortran/etc) that are not built for the Pyodide environment cannot be installed. See https://pyodide.org/en/stable/usage/faq.html#why-can-t-micropip-find-a-pure-python-wheel-for-a-package for the details.
Other problems are tracked at GitHub Issues: https://github.com/whitphx/stlite/issues If you find a new problem, please report it.
TL;DR: Use top-level await instead of asyncio.run()
on Stlite.
Unlike the original Streamlit, Stlite supports top-level await due to the differences in their execution models. Streamlit runs in a standard Python environment, allowing the use of asyncio.run()
when an async function needs to be executed within a script. In contrast, Stlite runs in a web browser, operating in an environment where the only event loop is always in a running state. This makes it impossible to use asyncio.run()
within a script, necessitating the support for top-level await.
Top-level await can be useful in various situations.
One of the most common use cases is asyncio.sleep()
. As mentioned in the previous section, time.sleep()
is no-op on Stlite because its blocking nature is not compatible with the single-threaded event loop in the web browser environment. Instead, asyncio.sleep()
, which is non-blocking, can be used to pause the execution of a script for a specified amount of time.
You can use top-level await either for asyncio.sleep()
directly or for an async function that contains asyncio.sleep()
like the following:
import asyncio
import streamlit as st
st.write("Hello, world!")
await asyncio.sleep(3)
st.write("Goodbye, world!")
import asyncio
import streamlit as st
async def main():
st.write("Hello, world!")
await asyncio.sleep(3)
st.write("Goodbye, world!")
await main()
Another common use case is accessing external resources. Pyodide provides a Python wrapper of browser's Fetch API, pyodide.http.pyfetch()
for making HTTP requests. Since this method is async, top-level await is sometimes used to handle the response.
Here's an example:
import pyodide.http
url = "your_url_here"
response = await pyodide.http.pyfetch(url)
data_in_bytes = await response.bytes()
- 📖 Streamlit meets WebAssembly - stlite, by whitphx: A blog post covering from some technical surveys to the usages of the online editor Stlite Sharing, self-hosting apps, and the desktop app bundler.
- 📺 "Serverless Streamlit + OpenCV Python Web App Tutorial", by 1littlecoder, YouTube: A quick tutorial to develop an OpenCV image processing app with Stlite that runs completely on browsers.
- 📖 "New library: stlite, a port of Streamlit to Wasm, powered by Pyodide", Streamlit Community: The Stlite thread at the Streamlit online forum.
- 📺 "Stlite - Streamlit without Server - powered by Pyodide (WebAssembly)", by 1littlecoder, YouTube: A quick tutorial with local app development and GitHub Pages deployment.
- 📺 "How to Convert a Streamlit App to an .EXE Executable", by Fanilo Andrianasolo, YouTube: A tutorial to convert a Streamlit app to an executable file with Stlite and a demo to run it on an offline machine.
- 📖 "Is This the Easiest Way to Build Your Streamlit App?", by Shantala Mukherjee
- 📖 "The Best Python Desktop App Framework?", by Caleb Robey at Depot Analytics
- 📖 "Python-Based Data Viz (With No Installation Required)", by Sam Minot
- 📖 "Converting Streamlit application to exe file", by Neelasha Sen
- 📖 "Streamlit + Stlite: Beyond Data Science Applications", by Saumitra Panchal
- 📖 "stlite: Serverless Streamlit — Run Your Apps in the Browser", by Alan Jones
Image processing with OpenCV works on the client side.
- Repository📌: https://github.com/whitphx/stlite-image-processing-app
- Online demo🎈: https://whitphx.github.io/stlite-image-processing-app/
See the tutorial video
Serverless Streamlit + OpenCV Python Web App Tutorial, crafted by 1littlecoder.
They are sponsoring me on GitHub Sponsors!
They are sponsoring me on GitHub Sponsors!
They are sponsoring me on GitHub Sponsors!