Fix: Streamlit Not Working — Session State, Cache, and Rerun Problems
Quick Answer
How to fix Streamlit errors — session state KeyError state not persisting, @st.cache deprecated migrate to cache_data cache_resource, file upload resetting, slow app loading on every interaction, secrets not loading, and widget rerun loops.
The Error
You click a button and every widget resets to its default value — the model output disappears, the dropdown reverts to the first option, and the uploaded file is gone.
Or you access session state and get a KeyError:
KeyError: 'st.session_state has no key "results". Did you forget to initialize it?'Or the app is slow because it reloads a 2GB model on every user interaction:
@st.cache # DeprecationWarning: st.cache is deprecated.
def load_model():
return torch.load("model.pt")Or you deploy to Streamlit Community Cloud and the app crashes because secrets aren’t loading:
KeyError: 'st.secrets has no attribute "API_KEY".'Most Streamlit confusion traces back to one root cause: the script re-executes from top to bottom on every user interaction. Understanding that model solves most issues before they require a fix.
Why This Happens
Streamlit’s execution model is intentionally simple: every button click, slider move, text input, or selectbox change triggers a full re-run of your Python script. Variables defined at the top of the script are reset. Functions are called again. Data is reloaded.
This design makes Streamlit easy to start with — you write a script, not a callback-heavy event loop. But it means any state you don’t explicitly preserve is lost on every interaction. The tools for preserving state are st.session_state (for values) and @st.cache_data/@st.cache_resource (for expensive computations).
Fix 1: Everything Resets on Interaction — The Rerun Model
Before fixing anything, understand what triggers a rerun:
- Any widget interaction (button click, slider, selectbox, text input)
st.rerun()called explicitly- The browser tab refreshing
Variables defined at script level are re-created fresh every rerun:
import streamlit as st
# This resets to 0 on every interaction — not a counter
count = 0
if st.button("Increment"):
count += 1 # Adds 1 to 0 every time — never goes above 1
st.write(count)Use session state to persist values across reruns:
import streamlit as st
# Initialize once — the `not in` check prevents resetting on each rerun
if "count" not in st.session_state:
st.session_state.count = 0
if st.button("Increment"):
st.session_state.count += 1
st.write(f"Count: {st.session_state.count}")Use st.form() to batch widget interactions and only trigger a rerun when the user submits — not on every individual widget change:
import streamlit as st
with st.form("search_form"):
query = st.text_input("Search query")
max_results = st.slider("Max results", 1, 100, 10)
submitted = st.form_submit_button("Search")
# Code below only runs after the form is submitted, not on every keystroke
if submitted:
st.write(f"Searching for: {query}, limit: {max_results}")Without a form, each keystroke in st.text_input triggers a full rerun — 10 keystrokes = 10 model calls.
Fix 2: Session State KeyError — Initialize Before Use
KeyError: 'st.session_state has no key "results".'
StreamlitAPIException: Session state key "results" does not exist.You’re accessing a key that hasn’t been set yet. Session state is empty on the first run of a fresh session.
Always guard with in before accessing:
import streamlit as st
# WRONG — KeyError on first run because 'results' doesn't exist yet
results = st.session_state["results"]
# CORRECT option 1 — check before accessing
if "results" in st.session_state:
st.write(st.session_state.results)
else:
st.info("Run the analysis to see results.")
# CORRECT option 2 — initialize at the top of the script
if "results" not in st.session_state:
st.session_state.results = None
if "model" not in st.session_state:
st.session_state.model = None
if "history" not in st.session_state:
st.session_state.history = []Attribute-style access (st.session_state.key) raises AttributeError instead of KeyError, but the same guard applies. Use st.session_state.get("key", default) for safe access with a fallback:
# Safe access with default — never raises
results = st.session_state.get("results", None)
history = st.session_state.get("history", [])Widget keys are automatically stored in session state. Setting a key parameter on any widget creates a session state entry that’s managed by Streamlit:
# This creates st.session_state["search_input"] automatically
query = st.text_input("Search", key="search_input")
# You can read it from session state (same value)
st.write(st.session_state.search_input)
# But don't manually set a widget's key in session state — it conflicts
# WRONG: st.session_state.search_input = "new value" (raises StreamlitAPIException)Fix 3: @st.cache Is Deprecated — Migrate to New Cache APIs
DeprecationWarning: st.cache is deprecated. Please use one of Streamlit's new caching decorators:
- st.cache_data for data objects (DataFrames, lists, dicts, etc.)
- st.cache_resource for shared resources (ML models, DB connections, etc.)@st.cache was removed in Streamlit 1.18 (February 2023). The replacement is two focused decorators:
| Old | New | Use for |
|---|---|---|
@st.cache | @st.cache_data | DataFrames, API responses, computed results |
@st.cache | @st.cache_resource | ML models, DB connections, shared singletons |
@st.experimental_memo | @st.cache_data | Same |
@st.experimental_singleton | @st.cache_resource | Same |
@st.cache_data — caches the return value, returns a copy each call:
import streamlit as st
import pandas as pd
@st.cache_data
def load_sales_data(file_path: str) -> pd.DataFrame:
"""Runs once per unique file_path argument, then returns cached copy."""
df = pd.read_csv(file_path)
df['date'] = pd.to_datetime(df['date'])
return df
# On first call: reads CSV, processes, caches
# On subsequent calls with same path: returns cached copy instantly
df = load_sales_data("data/sales_2025.csv")@st.cache_resource — caches the object itself, returns the same shared instance:
import streamlit as st
import torch
@st.cache_resource
def load_model(model_path: str):
"""Loaded once per unique path, shared across all sessions."""
model = torch.load(model_path, map_location='cpu')
model.eval()
return model
# Model loads once when the app starts, shared by all concurrent users
model = load_model("models/classifier.pt")Key difference: cache_data makes a copy per call (safe for mutable objects like DataFrames — mutations in one session don’t affect others). cache_resource shares one instance (right for models and connections that are expensive to create and safe to share).
Clear the cache when the underlying data changes:
# Clear all caches
st.cache_data.clear()
st.cache_resource.clear()
# Or add a "Refresh Data" button
if st.button("Refresh Data"):
st.cache_data.clear()
st.rerun()Force cache bypass with ttl (time-to-live):
@st.cache_data(ttl=3600) # Re-run function after 1 hour
def fetch_live_prices():
return requests.get("https://api.example.com/prices").json()Fix 4: App Is Slow — Data Reloads on Every Click
If your app feels sluggish, every click probably re-runs an expensive operation. Audit what runs on each interaction:
import streamlit as st
import pandas as pd
import time
# WRONG — this re-reads from disk on every button click, slider move, etc.
df = pd.read_csv("large_dataset.csv") # 5 seconds every rerun
# CORRECT — cached after first load
@st.cache_data
def load_data():
return pd.read_csv("large_dataset.csv")
df = load_data() # ~5s first run, instant afterMove all expensive operations into cached functions. The rule: if a function call takes more than 100ms and doesn’t need to re-run on every interaction, cache it.
import streamlit as st
from sklearn.ensemble import RandomForestClassifier
import numpy as np
@st.cache_resource # Model is shared — expensive to train
def train_model(n_estimators: int):
X = np.random.rand(10000, 20)
y = (X[:, 0] + X[:, 1] > 1).astype(int)
clf = RandomForestClassifier(n_estimators=n_estimators)
clf.fit(X, y)
return clf
@st.cache_data # Data objects — return copies
def get_test_data():
return np.random.rand(100, 20)
n_trees = st.slider("Number of trees", 10, 200, 100)
model = train_model(n_trees) # Re-trains only when n_trees changes
X_test = get_test_data()
predictions = model.predict(X_test)
st.write(f"Accuracy: {(predictions == (X_test[:, 0] + X_test[:, 1] > 1)).mean():.2%}")Use st.spinner() to show progress for operations that can’t be cached:
with st.spinner("Loading data..."):
df = fetch_from_database() # Shows spinner while loading
st.success("Done!")Fix 5: File Upload Resets After Interaction
# User uploads a file, selects options, clicks button — file disappears
uploaded_file = st.file_uploader("Upload CSV")
# File resets to None on the next rerunThe UploadedFile object lives in widget state for one rerun cycle. When any other widget changes (a dropdown selection, button click), the file uploader resets unless you explicitly store the contents.
Store file contents in session state immediately on upload:
import streamlit as st
import pandas as pd
# Save the file contents to session state when uploaded
uploaded_file = st.file_uploader("Upload a CSV file", type=["csv"])
if uploaded_file is not None:
# Read and store immediately — before any other interaction can reset it
if "uploaded_df" not in st.session_state or st.session_state.get("last_filename") != uploaded_file.name:
st.session_state.uploaded_df = pd.read_csv(uploaded_file)
st.session_state.last_filename = uploaded_file.name
# Now use the stored DataFrame — persists across reruns
if "uploaded_df" in st.session_state:
df = st.session_state.uploaded_df
st.write(f"Loaded {len(df)} rows")
# Other widgets won't reset the DataFrame
column = st.selectbox("Choose column", df.columns)
st.bar_chart(df[column])Reading different file types from UploadedFile:
import streamlit as st
import pandas as pd
import json
uploaded_file = st.file_uploader("Upload file", type=["csv", "json", "xlsx"])
if uploaded_file is not None:
file_type = uploaded_file.name.split(".")[-1].lower()
if file_type == "csv":
df = pd.read_csv(uploaded_file)
elif file_type == "xlsx":
df = pd.read_excel(uploaded_file)
elif file_type == "json":
data = json.load(uploaded_file) # UploadedFile is file-like
# Reset read position if reading multiple times
uploaded_file.seek(0)
raw_bytes = uploaded_file.read()Pro Tip: Use st.file_uploader("...", accept_multiple_files=True) to accept multiple files. It returns a list of UploadedFile objects — or an empty list when no files are uploaded, never None.
Fix 6: streamlit: command not found
$ streamlit run app.py
bash: streamlit: command not foundStreamlit installs a streamlit script into your Python environment’s bin/ or Scripts/ directory. If that directory isn’t on your PATH, the command isn’t found.
The immediate fix — run via Python module:
python -m streamlit run app.pyThis bypasses the PATH issue entirely.
Add the scripts directory to PATH:
# Find where streamlit is installed
python -m site --user-scripts # e.g., ~/.local/bin
# Add to PATH (bash)
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrcIf you’re using a virtual environment, activate it first:
source .venv/bin/activate
streamlit run app.pyCheck the installed version and location:
python -m streamlit --version
pip show streamlit # Shows Location fieldFor module-not-found errors if Streamlit installs successfully but app.py can’t import its dependencies, the environment issue is identical to the one covered in Python ModuleNotFoundError in venv.
Fix 7: Secrets and Environment Variables
Local development — store secrets in ~/.streamlit/secrets.toml:
# ~/.streamlit/secrets.toml (never commit this file)
API_KEY = "sk-abc123"
DATABASE_URL = "postgresql://user:pass@host:5432/db"
[database]
host = "localhost"
port = 5432
name = "mydb"Access in your app:
import streamlit as st
# Top-level keys
api_key = st.secrets["API_KEY"]
# Nested keys
db_host = st.secrets["database"]["host"]
# or
db_host = st.secrets.database.hostOn Streamlit Community Cloud — set secrets in the app settings UI (App → Settings → Secrets). The format is the same TOML structure. Never put secrets in requirements.txt, app.py, or any committed file.
Fall back to environment variables for local/CI flexibility:
import streamlit as st
import os
# Try Streamlit secrets first, fall back to environment variable
api_key = st.secrets.get("API_KEY") or os.environ.get("API_KEY")
if not api_key:
st.error("API_KEY not configured. Set it in .streamlit/secrets.toml or as an environment variable.")
st.stop() # Halts script execution cleanlyst.stop() is the correct way to abort the script after showing an error — it stops execution of all remaining code without throwing an exception.
requirements.txt for Community Cloud deployment must list all your dependencies explicitly:
# requirements.txt — place in the root of your GitHub repo
streamlit>=1.30.0
pandas>=2.0.0
scikit-learn>=1.3.0
plotly>=5.0.0
requests>=2.31.0Fix 8: Layout and Component Issues
Columns don’t wrap on narrow screens — Streamlit columns use CSS flexbox and don’t wrap automatically. On mobile or narrow windows, columns get squeezed. Set column widths as ratios and test on the target screen size:
import streamlit as st
# Equal columns
col1, col2, col3 = st.columns(3)
# Custom ratios — col1 is twice as wide as col2 and col3
col1, col2, col3 = st.columns([2, 1, 1])
# With gap
col1, col2 = st.columns(2, gap="large") # "small", "medium", "large"
with col1:
st.metric("Revenue", "$1.2M", "+12%")
with col2:
st.metric("Users", "42,000", "+8%")st.empty() for updating a single element in-place:
import streamlit as st
import time
placeholder = st.empty()
for i in range(10):
with placeholder.container():
st.write(f"Step {i+1}/10")
st.progress((i + 1) / 10)
time.sleep(0.5)
placeholder.empty() # Clear it when doneWithout st.empty(), each loop iteration adds a new element — you get 10 progress bars stacked.
Tabs:
import streamlit as st
tab1, tab2, tab3 = st.tabs(["Overview", "Data", "Model"])
with tab1:
st.header("Overview")
st.write("Summary here")
with tab2:
st.header("Data Explorer")
# Only runs when user clicks this tab... actually no:
# All tab code runs on every rerun — only display is deferredCommon Mistake: Assuming code inside with tab2: only runs when that tab is active. All code runs on every rerun — only the display is tab-gated. If tab content is expensive to compute, cache it:
@st.cache_data
def get_model_metrics():
# Expensive computation — cached regardless of which tab is active
return compute_metrics()
with tab2:
metrics = get_model_metrics() # Cached — fast even though code always runs
st.table(metrics)Still Not Working?
Multipage Apps
Create a pages/ directory alongside your main app.py. Any .py file in pages/ becomes a separate page in the sidebar navigation:
my_app/
├── app.py ← Main page
├── pages/
│ ├── 1_Data.py ← "Data" page
│ ├── 2_Model.py ← "Model" page
│ └── 3_Results.py ← "Results" page
└── requirements.txtSession state persists across page navigations within the same session. On page switch, Streamlit re-runs the new page’s script from the top.
Running on a Different Port
If port 8501 is taken, change it:
streamlit run app.py --server.port 8502For general port conflict diagnosis, the patterns in port already in use apply — find the PID using the port and kill it, or just choose a free port.
Docker Deployment
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
# Must bind to 0.0.0.0 — default localhost binding isn't reachable outside the container
CMD ["streamlit", "run", "app.py",
"--server.port=8501",
"--server.address=0.0.0.0"]Without --server.address=0.0.0.0, Streamlit binds to 127.0.0.1 inside the container and is unreachable from the host.
Using Streamlit with ML Models
For large ML models that need to stay loaded across sessions, @st.cache_resource is the right pattern — it loads the model once per server process, shared across all users. If you’re deploying scikit-learn models, see scikit-learn not working for patterns around model persistence with joblib. For Jupyter notebooks used in prototyping before converting to Streamlit apps, see Jupyter not working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Gradio Not Working — Share Link, Queue Timeout, and Component Errors
How to fix Gradio errors — share link not working, queue timeout, component not updating, Blocks layout mistakes, flagging permission denied, file upload size limit, and HuggingFace Spaces deployment failures.
Fix: Jupyter Notebook Not Working — Kernel Dead, Module Not Found, and Widget Errors
How to fix Jupyter errors — kernel fails to start or dies, ModuleNotFoundError despite pip install, matplotlib plots not showing, ipywidgets not rendering in JupyterLab, port already in use, and jupyter command not found.
Fix: LightGBM Not Working — Installation Errors, Categorical Features, and Training Issues
How to fix LightGBM errors — ImportError libomp libgomp not found, do not support special JSON characters in feature name, categorical feature index out of range, num_leaves vs max_depth overfitting, early stopping callback changes, and GPU build errors.
Fix: NumPy Not Working — Broadcasting Error, dtype Mismatch, and Array Shape Problems
How to fix NumPy errors — ValueError operands could not be broadcast together, setting an array element with a sequence, integer overflow, axis confusion, view vs copy bugs, NaN handling, and NumPy 1.24+ removed type aliases.