Streamlit Basics and Widgets - Interview Questions
1. How do you install and run a Streamlit app?
pip install streamlit
streamlit run app.py
That starts a local server (default http://localhost:8501) with a WebSocket connection back to the script. Editing app.py triggers a “Source file changed — Rerun?” prompt; auto-rerun is in Settings.
To run on a different port or bind address:
streamlit run app.py --server.port 8080 --server.address 0.0.0.0
2. What are the main widget types?
| Widget | Returns | Use for |
|---|---|---|
st.button("X") |
bool (True only on the rerun after click) |
One-shot actions |
st.text_input("X") |
str |
Single-line text |
st.text_area("X") |
str |
Multi-line text |
st.number_input("X") |
int/float |
Numbers with step/min/max |
st.slider("X", 0, 100) |
int/float/tuple |
Range selection |
st.selectbox("X", opts) |
option | Single-choice dropdown |
st.multiselect("X", opts) |
list |
Multi-choice |
st.radio("X", opts) |
option | Visible single-choice |
st.checkbox("X") |
bool |
Toggle |
st.date_input / st.time_input |
date / time |
Date/time pickers |
st.file_uploader("X") |
UploadedFile or None |
File upload |
st.color_picker("X") |
hex str |
Color |
st.toggle("X") |
bool |
Like checkbox, switch UI |
Every widget call returns its current value. The script reruns when the value changes.
3. How does st.button differ from other widgets?
st.button returns True only on the single rerun immediately after the click, then snaps back to False. Other widgets hold their value across reruns until the user changes them.
if st.button("Save"):
save_to_db() # runs once, on the click rerun
st.success("Saved!") # gone on the next rerun
# Anti-pattern: this won't work the way you expect
clicked = st.button("Click")
st.text_input("Name") # typing here reruns the script
# → clicked is False again, "Click" forgotten
To remember a click, write to st.session_state:
if st.button("Start"):
st.session_state["started"] = True
if st.session_state.get("started"):
show_workflow()
4. How do you lay out content?
import streamlit as st
# Sidebar
with st.sidebar:
page = st.radio("Section", ["A", "B"])
# Columns
col1, col2, col3 = st.columns([2, 1, 1]) # ratio
with col1:
st.metric("Revenue", "$1.2M")
with col2:
st.metric("Users", "12,300")
with col3:
st.metric("Latency", "230ms")
# Tabs
tab1, tab2 = st.tabs(["Overview", "Details"])
with tab1:
st.write("Summary view")
with tab2:
st.dataframe(df)
# Expander
with st.expander("Show raw data"):
st.json(payload)
# Container — group elements, return a handle you can write into later
placeholder = st.container()
placeholder.write("This appears here, even if added later.")
st.empty() reserves a slot you can overwrite — useful for replacing a “Loading…” placeholder with the final result.
5. How do you display data?
st.write(anything) # smart dispatcher: dict→json, df→table, str→markdown
st.dataframe(df) # interactive table, sortable, scrollable
st.table(df) # static, simpler, no virtualization
st.metric("Revenue", "$1M", "+5%") # KPI card with delta
st.json(my_dict)
st.code("print('hi')", language="python")
st.markdown("**bold**", unsafe_allow_html=False) # default is safe; html disabled
st.image("logo.png", width=200)
st.video(url_or_bytes)
st.audio(bytes)
For charts:
st.line_chart(df) # built-in, fast, limited
st.bar_chart(df)
st.area_chart(df)
st.altair_chart(chart, use_container_width=True)
st.plotly_chart(fig, use_container_width=True)
st.pyplot(fig) # matplotlib
Plotly is the default for “real” charts because it ships interactivity (hover, zoom) without extra work.
6. What’s the difference between st.write and st.markdown / st.text?
st.write(x)— smart. Dispatches based on type:dict→json,pd.DataFrame→table,str→markdown, plotly figure→chart, etc. Good for “just show me this.”st.markdown(s)— explicit markdown. Use when you know it’s text and want full control over formatting.st.text(s)— fixed-width plain text, no markdown rendering. Use for log lines or anything where you don’t want*foo*to become italic.
In production code, prefer the explicit form (markdown/dataframe/plotly_chart) — st.write is great for prototyping but ambiguous at review time.
7. How do you upload and process a file?
uploaded = st.file_uploader("Upload CSV", type=["csv"])
if uploaded is not None:
df = pd.read_csv(uploaded) # uploaded behaves like a file object
st.dataframe(df)
# For binary files / re-reads:
raw = uploaded.read() # bytes
uploaded.seek(0) # reset pointer if you read it again
Multiple files:
files = st.file_uploader("Upload", accept_multiple_files=True)
for f in files:
process(f)
Defaults: 200 MB per file. Override via .streamlit/config.toml → [server] maxUploadSize = 1024 (in MB).
8. How do you show progress for a long operation?
with st.spinner("Loading..."):
result = slow_function()
# or a numeric progress bar
bar = st.progress(0, text="Processing...")
for i, item in enumerate(items):
process(item)
bar.progress((i + 1) / len(items), text=f"{i+1}/{len(items)}")
bar.empty()
# Status block (1.31+) — collapsible, shows steps
with st.status("Running pipeline...", expanded=True) as status:
st.write("Downloading data...")
download()
st.write("Training model...")
train()
status.update(label="Done", state="complete", expanded=False)
# Toast — small ephemeral notification (3s)
st.toast("Saved!", icon="")
For long jobs, also consider running them outside the Streamlit script (Celery / a queue) and polling status — Streamlit ties up a WebSocket per running script.
9. How do you handle forms (batch widget input)?
By default, every widget change reruns the script. For a form (multiple inputs that should only submit together), use st.form:
with st.form("user_form"):
name = st.text_input("Name")
age = st.number_input("Age", 0, 120)
submitted = st.form_submit_button("Save")
if submitted:
save_user(name, age)
Inside st.form, widget changes don’t trigger reruns — only the submit button does. This is the right tool for any “fill out fields, then submit” interaction.
10. Cross-links
- Execution model & rerun: 00_streamlit_overview.md
- Persisting widget state across reruns: 02_session_state.md
- Caching expensive computations: 03_caching.md
Interview angle
- “How does a widget’s value persist across reruns?” - by its key. Streamlit identifies widgets by position and parameters unless you pass an explicit
key, which is why conditionally rendered widgets can lose or swap state. - “Why does my widget reset unexpectedly?” - its identity changed. Adding a widget above it, or changing its label, changes the auto-generated key. Explicit keys fix it.
- “How do you avoid work on every keystroke?” -
st.formbatches inputs so the script only re-runs on submit, and caching handles the expensive parts elsewhere.