Streamlit Testing and Advanced Topics - Interview Questions
1. How do you test a Streamlit app?
Streamlit ships AppTest (in streamlit.testing.v1) — a headless test harness that runs your script and lets you assert on widgets and outputs.
# tests/test_app.py
from streamlit.testing.v1 import AppTest
def test_counter_increments():
at = AppTest.from_file("app.py").run()
assert at.session_state["count"] == 0
at.button[0].click().run()
assert at.session_state["count"] == 1
assert at.markdown[0].value == "Count: 1"
It runs in the same process as pytest — no browser, no WebSocket, fast. Asserts:
at.button[i] / at.text_input[i] / at.selectbox[i]— widget access by type and index, or bykey=.at.markdown / at.dataframe / at.title— output elements.at.session_state— same dict you read in the app.at.exception— list of exceptions raised during the run.
Pattern: load the script, simulate interactions with .click() / .set_value(...), call .run() again, assert.
2. How do you simulate widget interactions in tests?
at = AppTest.from_file("app.py").run()
at.text_input(key="name").set_value("Alice").run()
at.selectbox(key="role").select("admin").run()
at.checkbox(key="terms").check().run()
at.slider(key="age").set_value(30).run()
at.button(key="submit").click().run()
assert at.markdown[-1].value == "Welcome, Alice (admin)"
.set_value() / .click() mutates the widget; .run() re-executes the script with the new state. Chain them for multi-step flows.
For session state preconditions, set them before the first run:
at = AppTest.from_file("app.py")
at.session_state["user"] = {"id": 1, "name": "Alice"}
at.run()
3. What is st.fragment and when do you use it?
@st.fragment (1.33+) marks a function whose interactions trigger a partial rerun — only the fragment re-executes, not the whole script.
import streamlit as st
st.title("Big dashboard")
expensive_data = load_million_rows() # not re-run on fragment interactions
@st.fragment
def filter_panel():
region = st.selectbox("Region", ["US", "EU", "APAC"])
metric = st.selectbox("Metric", ["revenue", "users"])
st.bar_chart(expensive_data.query("region == @region")[metric])
filter_panel()
Clicking inside filter_panel() re-runs only the fragment. The expensive load_million_rows() outside doesn’t fire.
Use for: filter panels, embedded charts, any sub-component whose updates shouldn’t ripple to the whole page.
@st.fragment(run_every="5s") makes the fragment auto-rerun on a timer — cheap polling for live dashboards.
4. How do you handle background work / long-running tasks?
Streamlit’s script is foreground; long jobs block the UI. Three patterns:
1. Threads with shared state.
import threading
import streamlit as st
def run_job(job_id):
result = expensive_computation()
st.session_state[f"job_{job_id}"] = result # WARNING: see below
if st.button("Start"):
job_id = uuid.uuid4().hex
threading.Thread(target=run_job, args=(job_id,), daemon=True).start()
st.session_state["last_job"] = job_id
# Need to add the script context for session_state to work cross-thread:
from streamlit.runtime.scriptrunner import add_script_run_ctx
ctx = get_script_run_ctx()
t = threading.Thread(target=run_job, args=(...,))
add_script_run_ctx(t, ctx)
t.start()
2. Offload to a real task queue (Celery / RQ / Cloud Tasks). App enqueues; the result lands in DB; Streamlit polls.
3. st.fragment(run_every="2s") to poll status.
@st.fragment(run_every="2s")
def status_panel():
job = db.get_job(st.session_state["job_id"])
st.write(job.status)
if job.status == "done":
st.success("Done!")
st.write(job.result)
For anything past 30s, use option 2. Don’t tie up Streamlit threads for batch work.
5. How do you build a custom component?
When the built-in widgets don’t cover what you need, write a Streamlit Component — a React (or any-framework) widget that talks to the Python script via JSON messages.
Two flavors:
- Static component — pure HTML/JS injected via
components.html(...). No two-way data flow. - Bidirectional component — built with
streamlit-component-lib(npm) andstreamlit.components.v1.declare_component(Python). React frontend renders, callsStreamlit.setComponentValue(...), Python reads the return value.
import streamlit.components.v1 as components
# Static
components.html("<div>Hello from raw HTML</div>", height=100)
components.iframe("https://example.com", height=400)
# Declared (custom)
my_widget = components.declare_component("my_widget", path="./frontend/build")
result = my_widget(label="Pick", default=None)
In practice, before writing one, check the community list (streamlit-aggrid, streamlit-folium, streamlit-elements, streamlit-extras) — someone has probably already built it.
6. How do you use st.context and HTTP headers?
st.context (1.35+) exposes request metadata: headers, cookies, IP, locale.
user = st.context.headers.get("X-Forwarded-User")
locale = st.context.locale # e.g., "en-US"
session_cookie = st.context.cookies.get("session")
Used for:
- Auth — read user from a proxy-set header.
- i18n — pick translation by locale.
- Audit logging — log which user did what.
Headers from a reverse proxy must be trusted; spoofable if the app is reachable directly. Always set enableXsrfProtection and bind only to internal interfaces when behind a proxy.
7. How do you handle errors and graceful failure?
try:
df = load_data()
except FileNotFoundError:
st.error("Data file not found — please re-upload.")
st.stop() # halts the rest of the script
except Exception as e:
st.exception(e) # nice traceback display
st.stop()
st.error/st.warning/st.success/st.info— colored banner messages.st.exception(e)— shows the exception object with traceback.st.stop()— cleanly halts script execution. Nothing below runs.
For production, set client.showErrorDetails = false in config and route exceptions to Sentry — users shouldn’t see Python tracebacks.
8. How do you theme a Streamlit app?
.streamlit/config.toml:
[theme]
base = "light" # or "dark"
primaryColor = "#1f77b4"
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f0f2f6"
textColor = "#262730"
font = "sans serif"
Or build a custom theme through Settings → Theme in the UI, copy the resulting TOML into config.
For per-component CSS overrides, inject CSS via st.markdown(unsafe_allow_html=True):
st.markdown(
"<style>.stButton button { border-radius: 999px; }</style>",
unsafe_allow_html=True,
)
This is brittle — Streamlit’s internal class names can change between versions. Use sparingly.
9. How do you measure performance / find what’s slow?
- Profile a function — wrap with
cProfile, log results. - Time blocks —
time.perf_counter()around expensive calls; log the delta. - Check the rerun trigger — open browser devtools → Network → WS, watch the
rerunScriptmessages. If reruns are firing on every keystroke, something is bound to atext_inputoutside ast.form. - Cache audit — count
@st.cache_data/@st.cache_resourcehits/misses. A cached function that’s still slow is a cold-cache problem; a function that should be cached but isn’t is missing the decorator. - Memory —
@st.cache_resourcekeeps objects alive in process memory. Large models per pod = OOM. Track resident memory (psutil.Process().memory_info().rss).
Common culprits in slow Streamlit apps: un-cached data loads on every rerun, large DataFrames passed through st.dataframe without filtering, inline LLM calls outside an if prompt: guard, blocking I/O in the main script.
10. Common advanced pitfalls
| Pitfall | Fix |
|---|---|
Threads can’t access st.session_state |
Attach script context with add_script_run_ctx(thread, ctx) |
| Custom CSS breaks after a Streamlit upgrade | Avoid CSS overrides; use theme + components |
st.fragment doesn’t share state |
It does — fragments read/write st.session_state like the main script, but their reruns are isolated to the fragment |
| Tests pass locally, fail in CI | Headless AppTest is reproducible; usually a missing test dependency or a path difference (AppTest.from_file is relative to cwd) |
st.write_stream returns a generator, not a string |
Capture the return value; it’s the joined result |
| Memory leak across reruns | Resources cached with @st.cache_resource persist; ensure you’re not calling .clear() and reloading on every rerun |
| App freezes during a long synchronous LLM call | Always stream. If you can’t stream, run in a thread + poll, or offload to a queue |
11. Cross-links
- Async patterns / background jobs: ../../../system_design/03_async_patterns/
- Caching deep dive: 03_caching.md
- Pytest fundamentals: ../../05_testing/
- Custom components ecosystem:
streamlit-extras,streamlit-aggrid,streamlit-elements
Interview angle
- “How do you test a Streamlit app?” -
AppTestruns the script without a browser and lets you assert on widget values and rendered output. The stronger approach is keeping business logic in plain modules with their own tests and leaving Streamlit as a thin view layer. - “How do you keep it maintainable?” - separate data access and computation from presentation. A Streamlit script that also holds business logic is untestable and hard to reuse.
- “What are the common performance problems?” - uncached expensive calls re-running on every interaction, loading large datasets per rerun, and unbatched inputs. Caching plus
st.formfixes most of them.