A simplified mini version of Streamlit Notebook that runs only with run_mini_notebook.py and notebook_imports.py.
To run it, simply do:
streamlit run run_mini_notebook.pyThe reactive notebook powered by Streamlit.
Streamlit Notebook combines the interactive development experience of Jupyter notebooks with the deployment simplicity of Streamlit apps. Write code in notebook-style cells with a professional-grade Python shell that maintains state across reruns, then deploy the exact same file as a production-ready Streamlit application.
We all know Jupyter as the go-to for interactive programming, but it comes with its own limitations:
- JSON format notebooks
- converting notebooks to deployable apps means rewriting everything.
- limited UI widget ecosystem,
- limited UI/namespace reactivity
- limited dynamic creation of widgets
- limited programmatic creation and execution of cells
- kernel / frontend dichotomy
- Huge dependency tree
Streamlit on the other hand is great for fast reactive apps development, with a huge components ecosystem, and easy deployment on the cloud. But its "rerun the whole script" execution model (without namespace persistence!) can sometimes turn cumbersome, and it lacked a proper notebook ergonomics.
Streamlit Notebook attempts to give you the best of both worlds.
- Development: Cell-by-cell execution, full widget support, selective reactivity, persistent namespace, fast iteration, saved as readable and runnable
.pyfiles. - Deployment: Easily publish your notebook as a Streamlit app, no special runtime, deploy in couple of clicks, works anywhere Streamlit works.
Try the hosted demo: st-notebook.streamlit.app
pip install streamlit-notebookThe package comes equiped with the latest version of streamlit and a comprehensive starter data-science pack including:
matplotlib, numpy, seaborn, pandas, pillow, openpyxl, requests, pydub, graphviz, altair, plotly, bokeh, pydeck, scipy, sympy, scikit-learn, vega-datasets
Option 1: Start from the UI
st_notebook # Opens empty notebook interfaceCreate cells interactively, then click "Save notebook" to save as a .py file.
Option 2: Write the file directly
# analysis.py
from streamlit_notebook import st_notebook
import streamlit as st
st.set_page_config(page_title="Analysis")
# Create a notebook instance
nb = st_notebook(title='analysis')
# Define cells
@nb.cell(type='code')
def setup():
import pandas as pd
df = pd.read_csv("data.csv")
print(f"Loaded {len(df)} rows")
@nb.cell(type='code', reactive=True)
def explore():
col = st.selectbox("Column", df.columns)
st.line_chart(df[col])
# Render the notebook
nb.render()You can open it with the st_notebook entry point
st_notebook analysis.py # Development mode - full notebook interface
st_notebook analysis.py --app # Locked app mode - clean interface, code cells hiddenOr run it directly with Streamlit! (same result)
streamlit run analysis.py # Development mode
streamlit run analysis.py --app # Locked app modeA bit of magic needs to happen under the hood to make it possible
st_notebookfirst attempts to get an existing notebook instance fromst.session_state, if none is found, it creates one.- The
@celldecorator is used to capture the source code of the functions' bodies and add the corresponding cells to the notebook instance. This happens only at the first pass of the script, and the decorator is no-oped afterwards (to avoid adding the same cells over and over as the script reruns). nb.render()finally takes care of fetching and displaying the current notebook instance from state.
Subsequent runs of the script will ignore the cell definitions and merely loop on nb = st_notebook(...) and nb.render() to refresh whatever notebook instance is living in the session's state.
Note: the functions defining the cells will never get called. Doing so would result in errors, as they refer to variables defined out of their local scopes (in other cells!). It's really a nice thing here that python allows to define erroneous function objects, even decorate them, without throwing an exception as long as we don't attempt to call them (lazy evaluation). They still know the file and line range in which they are defined, which is enough for the decorator to retrieve their raw source code. Makes them usable as mere "code bags", ie. containers for source code that gets extracted and executed elsewhere.
One-shot cells: Run only once when you click Run. Used for imports, data loading, expensive computations.
Reactive cells: Toggle the Reactive option on any code cell to make it reactive. These will rerun automatically on every UI interaction and update the python namespace accordingly. Used for widgets and reactive displays.
This selective reactivity lets you separate expensive setup from interactive exploration.
All cells execute in a shared long-lived Python session. Unlike regular Streamlit apps that restart from scratch on every rerun, imports, variables, and computed results persist across UI interactions.
Example:
@nb.cell(type='code')
def load_data():
import pandas as pd
df = pd.read_csv("large_file.csv") # One shot cell, runs only once
@nb.cell(type='code', reactive=True)
def explore():
threshold = st.slider("Threshold", 0, 100)
st.write(df[df['value'] > threshold]) # no need to redefine df, even after rerunsCell 1 loads data once. Cell 2 reruns on slider changes. Both execute in the same namespace.
Every Streamlit widget, chart, and component works out of the box. Just copy-paste your existing Streamlit code into cells.
@nb.cell(type='code', reactive=True)
def widgets():
# Standard Streamlit code - nothing new to learn
value = st.slider("Select value", 0, 100)
st.metric("Current value", value)
st.bar_chart([value, value*2, value*3])Building a stock price analysis dashboard. This example uses real data from the vega_datasets package (included) and can be copy-pasted and run directly:
from streamlit_notebook import st_notebook
import streamlit as st
st.set_page_config(page_title="Stock Dashboard", layout="wide")
nb = st_notebook(title='stock_dashboard')
@nb.cell(type='code')
def setup():
import pandas as pd
import altair as alt
from vega_datasets import data
@nb.cell(type='code')
def load_data():
df = data.stocks()
df['date'] = pd.to_datetime(df['date'])
print(f"Loaded {len(df):,} records for {df['symbol'].nunique()} stocks")
@nb.cell(type='code', reactive=True)
def filters():
st.markdown("### Stock Analysis Dashboard")
symbols = st.multiselect("Select stocks", df['symbol'].unique(), default=['AAPL', 'GOOG'])
date_range = st.date_input("Date range", [df['date'].min(), df['date'].max()])
@nb.cell(type='code', reactive=True)
def dashboard():
filtered = df[df['symbol'].isin(symbols)] if symbols else df
if date_range and len(date_range) == 2:
filtered = filtered[(filtered['date'] >= pd.Timestamp(date_range[0])) &
(filtered['date'] <= pd.Timestamp(date_range[1]))]
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Selected Stocks", len(symbols))
with col2:
st.metric("Avg Price", f"${filtered['price'].mean():.2f}")
with col3:
st.metric("Total Records", f"{len(filtered):,}")
chart = alt.Chart(filtered).mark_line().encode(
x='date:T',
y='price:Q',
color='symbol:N'
).properties(height=400)
st.altair_chart(chart, use_container_width=True);
nb.render()Once you're done working on your notebook you may run it using:
streamlit run sales_dashboard.py --appNow the same file runs as a locked production app with restricted interface and no visible/editable code. This prevents untrusted users to run arbitrary code in the cloud environment. Your notebook can thus safely be published as an online app without more overhead.
The dedicated environment variable does the same as the flag:
export ST_NOTEBOOK_APP_MODE=true # or use a .env file
streamlit run sales_dashboard.pyUseful when you can't modify the command directly (e.g., in Streamlit cloud platform).
Notebook Mode (development):
- Code editor for each cell
- Cell management (create, delete, change type, reorder)
- Execution controls (Run Next, Run All, Restart Session, Clear All Cells)
- Save/Open notebooks
- Demo notebooks library
App Mode (deployment):
- Restricted interface
- Code editors hidden
- Interactive widgets remain functional
- Clean Streamlit appearance
In development, you may toggle between modes with the sidebar switch, or run with enforced app mode to prevent users toggling back to notebook mode.
Rich display
Cell results are automatically displayed with pretty formatting when possible (anything that st.write can handle).
Control how expression results appear:
all- show every expression resultlast- show only the last expression (default)none- suppress automatic display
Configurable in the sidebar settings.
You may also :
- add a trailing
;at the end of a line to bypass automatic display - use the
displayfunction to selectively display a given result
Markdown/HTML cells
Add rich formatted text or layouts to your notebook with Markdown and HTML cells. They support variable interpolation using the <<any_expression>> syntax. The expression will be evaluated and replaced in the code.
If the value changes, the displayed content will change as well.
# Analysis Results
We loaded << len(df) >> rows.
The mean value is << df['value'].mean() >>.System commands and magics
Ipython style commands and magics are supported. Let's demonstrate this by defining a new magic to integrate a basic AI assistant in the session.
#Cell 1
#system command to install the openai package
!pip install openai
#simple agent class
class Agent:
def __init__(self):
from openai import OpenAI
self.messages=[]
self.client=OpenAI() #we use the OPENAI_API_KEY provided as env variable
def add_message(self,**kwargs):
self.messages.append(kwargs)
def __call__(self,prompt):
self.add_message(role="user",content=prompt)
response=self.client.chat.completions.create(
model="gpt-4.1-mini",
messages=self.messages
).choices[0].message.content
self.add_message(role="assistant",content=response)
return response
#create an agent instance
agent=Agent()
#define a new magic to call our agent
@magic
def ai(text_content):
if text_content:
response=agent(text_content)
display(response) # we use display instead of print for prettier markdown rendering
#call it in a single line starting with % (takes the rest of the line after the %<command> as input string)
%ai Hello, this is a test!With %%, the whole cell starting at second line is considered the magic input.
#Cell 2
%%ai
All the content of
the cell goes in the
magic inputNote: Only the mechanism is supported, no predefined magics are provided (yet) so you have to declare your own magics.
Warning: contrary to Ipython, ! and !! here work the same as % and %%, namely they distinguish between single line and full-cell magics. They just execute the input as a system command/script.
Disclaimer: The shell is NOT meant to be an exact replica of Ipython. My goal is to provide a practical and versatile coding environment matching the common needs of interactive programming. Yet, you might encounter situations where you feel like some useful features of Ipython are missing, if so please add a feature request.
You may toggle the "Fragment" option of a reactive cell to run the cell as a Streamlit fragment for faster, scoped updates:
@nb.cell(type='code', reactive=True, fragment=True)
def fast_widget():
# Only this cell reruns on interaction
value = st.slider("Value", 0, 100)
st.write(f"Selected: {value}")This way the page reloads only the UI fragment in which the interaction happens. Beware that other widgets on the page won't refresh even if they depend on variables changed by the fragment. So, in general, it's better to group in a same fragment subsets of widgets and that are supposed to react to eachother.
Note: A variable that's changed by a fragment is immediately updated in the namespace and can be used elsewhere in the notebook. The isolation is just ui-side, not backend-side.
The notebook object is exposed in the shell's namespace as __notebook__ and can be controled programmaticaly from code cells — useful for automation or AI agents:
# get the notebook instance
nb=__notebook__
# Create cells programmatically
cell = nb.new_cell(
type="code",
code="st.line_chart([1,2,3])",
reactive=True
)
cell.run()
# Edit existing cells
nb.cells[0].code = "import pandas as pd"
nb.cells[0].run()Not really possible in Jupyter or very hacky!
From the interface:
- Save button: saves to
./notebook_title.py - Open button: dropdown of all
.pynotebooks in current directory or drag/drop/browse - Demo notebooks: load pre-built examples
From code:
__notebook__.save()
__notebook__.save("my_notebook.py")
__notebook__.open("my_notebook.py")First make sure your notebook looks and runs nice as an app.
# Test in locked app mode (production simulation)
st_notebook my_notebook.py --app-
Create
requirements.txt:streamlit-notebook # ... other dependencies -
Create
.streamlit/config.toml(optional - for page config):[theme] primaryColor = "#F63366" backgroundColor = "black"
-
Create
.envfile to enable locked app mode:ST_NOTEBOOK_APP_MODE=true
-
Push to GitHub:
git add my_notebook.py requirements.txt .env git commit -m "Add dashboard" git push -
Deploy on share.streamlit.io:
- Connect your GitHub repo
- Select
my_notebook.pyas main file - Click "Deploy"
Since notebooks are just Python files, Streamlit Cloud runs them directly — no wrapper needed.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY my_notebook.py .
EXPOSE 8501
# Option 1: Use --app flag
CMD ["streamlit", "run", "my_notebook.py", "--app"]
# Option 2: Use environment variable
# ENV ST_NOTEBOOK_APP_MODE=true
# CMD ["streamlit", "run", "my_dashboard.py"]Build and run:
docker build -t my-dashboard .
docker run -p 8501:8501 my-dashboardDeploy to AWS ECS, Google Cloud Run, Azure Container Apps, or any container platform.
✅ Do:
- Use one shot cells for expensive operations
- Test with
--appflag locally first - Add
--appflag to deployment command or set environment variables - Include all dependencies in
requirements.txt - Use
st.secretsfor secrets (API keys, database credentials, etc.)
❌ Don't:
- Allow code editing in production deployments (the user could read
st.secretsor run malicious scripts) - Hardcode API keys or credentials.
- Assume filesystem persistence. Changes you make to the files will be discarded when the container reboots. (use databases/cloud storage instead)
Deploy multiple related notebooks in a single repo, allowing users to navigate between them while keeping everything in locked app mode.
Use case: A data engineer creates several analysis notebooks (Sales, Customers, Forecasting) and wants to deploy them all as apps with simple navigation.
Setup:
my-analytics/
├── .env # ST_NOTEBOOK_APP_MODE=true
├── requirements.txt
├── sales_analysis.py
├── customer_segmentation.py
└── forecasting_model.py
How it works:
- Deploy to Streamlit Cloud pointing to any notebook as the main file
- Users see "Open notebook" dropdown to switch between notebooks
ST_NOTEBOOK_APP_MODE=truein.envensures ALL notebooks are locked apps- Safe for end users—no code editing allowed on any notebook
This is perfect for demos, dashboards, or sharing multiple analyses with stakeholders without creating separate deployments for each notebook.
Data Exploration → Dashboard: Build analysis interactively, deploy as stakeholder dashboard.
Prototyping → Production: Develop proof-of-concept in notebook mode, deploy as locked app without rewriting.
Interactive Reports: Blend narrative (Markdown) with live data and widgets.
Teaching & Demos: Create interactive tutorials for step-by-step learning.
AI Agent Integration: Let LLMs generate and execute cells programmatically for coding assistance and autonomous analysis.
Contributions welcome! File issues for bugs or feature requests. Submit PRs for improvements.
- Fork the repo
- Create a feature branch (
git checkout -b feature/my-idea) - Commit changes (
git commit -m "Add feature") - Push (
git push origin feature/my-idea) - Open a pull request
MIT License—see LICENSE.
Major update: Notebooks are now pure Python files (.py), not JSON.
- Pure Python format with
@nb.cell()decorator syntax - Self-contained notebook .py files
- Run directly with
streamlit run notebook.py - Locked App mode deployment option
- Removed
.stnbJSON format entirely
- Improved shell behaviour
- Implemented basic magic commands support
.stnbJSON format as defaultst_notebookaccepts file paths or JSON strings
- Custom shell with AST-based execution
- Expression display modes
- HTML cells
- Demo notebooks
Develop with notebooks. Deploy as apps. 🚀