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.

RequirementBuilt-in Feature
Loading feedbackst.spinner()
KPIsst.metric()
Layoutst.columns(), st.tabs()
Progressst.status()
Chat UIst.chat_message()
Streamingst.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

  1. Built-in
  2. Official extensions
  3. Third-party components
  4. 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.