Streamlit – Building Reliable Streamlit Applications
1. The Script Executes Top-to-Bottom on Every Interaction
Streamlit does not update incrementally. Any interaction—button, slider, input—causes a full script execution.
# ❌ State resets every time
counter = 0
if st.button("Add"):
counter += 1
st.write(counter)
# ✅ Persist state explicitly
if "counter" not in st.session_state:
st.session_state.counter = 0
if st.button("Add"):
st.session_state.counter += 1
st.write(st.session_state.counter)
Implication:
- Variables are ephemeral unless persisted
- UI actions are not incremental events
- The script is effectively stateless unless you define state
Mechanism: st.session_state
Use it for:
- Counters
- Multi-step flows
- Chat history
- User selections
2. Cache Any Expensive Operation
Given the full rerun model, any non-trivial computation will repeat unless explicitly cached.
# ❌ Re-executes every time
data = pd.read_csv("large_file.csv")
# ✅ Cached execution
@st.cache_data
def load_data():
return pd.read_csv("large_file.csv")
data = load_data()
Cache Types
@st.cache_data→ returns copies (DataFrames, API responses)@st.cache_resource→ returns shared objects (models, DB connections)
@st.cache_data(ttl=300)
def fetch_data():
return api_call()
Effect: avoids redundant computation across reruns.
3. Externalize Secrets
Hardcoding credentials introduces immediate security risk.
# ❌ Unsafe api_key = "sk-xxxx"
# ✅ Correct approach api_key = st.secrets["openai"]["api_key"]
Required setup
.streamlit/secrets.toml- Add to
.gitignore - Use platform secret management in deployment
4. Treat Missing Data as the Default Case
Streamlit apps frequently start in an empty state. Code must handle that condition explicitly.
uploaded = st.file_uploader("Upload")
if uploaded is not None:
df = pd.read_csv(uploaded)
st.dataframe(df)
else:
st.info("Upload a file to proceed")
Apply consistently
- File inputs
- API responses
- Filtered datasets
- Dynamic option lists
options = [x for x in items if x.startswith("A")]
if options:
st.selectbox("Choose", options)
else:
st.warning("No results available")
5. Use Forms to Control Execution Timing
Without forms, each input triggers a rerun independently.
# ❌ Fragmented execution
name = st.text_input("Name")
email = st.text_input("Email")
# ✅ Controlled submission
with st.form("user_form"):
name = st.text_input("Name")
email = st.text_input("Email")
submitted = st.form_submit_button("Submit")
if submitted:
if name and email:
process(name, email)
else:
st.error("Missing input")
Use when:
- Multiple dependent inputs
- Data entry workflows
- Validation is required before processing
6. Assign Explicit Keys to Dynamic Widgets
Streamlit identifies widgets by structure. In loops, this leads to collisions.
# ❌ Ambiguous widget identity
for item in items:
st.checkbox(item)
# ✅ Unique identifiers
for i, item in enumerate(items):
st.checkbox(item, key=f"checkbox_{i}")
Two access patterns
value = st.slider("Temperature", 0, 100, 50)
st.slider("Humidity", 0, 100, 50, key="humidity")
st.write(st.session_state.humidity)
Rule: Any dynamically generated widget requires a deterministic key.
7. Provide Feedback for Long-Running Operations
Unresponsive UI is interpreted as failure.
# ❌ No feedback result = slow_call()
# ✅ Spinner
with st.spinner("Processing..."):
result = slow_call()
# ✅ Multi-step status
with st.status("Running...", expanded=True) as status:
st.write("Loading data...")
data = load()
st.write("Processing...")
output = compute(data)
status.update(label="Done", state="complete")
Streaming responses (LLMs)
st.write_stream(stream)
Objective: maintain user trust during latency.
8. Use Fragments to Isolate Reruns
Full reruns can be inefficient when only part of the UI changes.
@st.fragment
def counter():
if st.button("Increment"):
st.session_state.count = st.session_state.get("count", 0) + 1
st.write(st.session_state.get("count", 0))
plot_heavy_chart(data) counter()
Effect:
- Expensive components remain static
- Interactive sections update independently
9. Enforce Project Structure Early
Unstructured scripts become unmanageable quickly.
my_app/ ├── app.py ├── requirements.txt ├── .streamlit/ │ ├── config.toml │ └── secrets.toml ├── pages/ ├── utils/
Key constraints
- Pin dependencies (
pandas==2.1.0) - Use relative paths
- Separate logic from UI
Result: reproducible deployments.
10. Prefer Built-in Components Over Custom Implementations
Streamlit provides native solutions for most UI needs.
| Requirement | Built-in Feature |
|---|---|
| Loading feedback | st.spinner() |
| KPIs | st.metric() |
| Layout | st.columns(), st.tabs() |
| Progress | st.status() |
| Chat UI | st.chat_message() |
| Streaming | st.write_stream() |
| Partial updates | @st.fragment |
# ❌ Manual formatting
st.write(f"Revenue: ${revenue}")
# ✅ Native component
st.metric("Revenue", f"${revenue}", f"{delta:+.1%}")
Selection hierarchy
- Built-in
- Official extensions
- Third-party components
- Custom implementation
Summary
Streamlit’s behavior is deterministic once its execution model is understood:
- Full script reruns drive everything
- State must be explicitly persisted
- Performance depends on caching
- UI stability depends on controlled execution
These rules eliminate the majority of non-obvious failures and enable predictable application behavior.