backend / web frameworks / streamlit / 04_multipage_apps.md

Streamlit Multipage Apps - Interview Questions

3 interview angles 4 min read source

Streamlit Multipage Apps - Interview Questions

1. How do you build a multipage app in Streamlit?

Two ways — pick one:

Modern (Streamlit ≥ 1.36): st.navigation + st.Page — explicit, code-defined, supports dynamic pages.

# app.py
import streamlit as st

pages = [
    st.Page("home.py", title="Home", icon=":material/home:"),
    st.Page("dashboard.py", title="Dashboard"),
    st.Page("admin.py", title="Admin"),
]

pg = st.navigation(pages)
pg.run()

Legacy (still supported): pages/ directory. Drop scripts into a pages/ folder next to your main script:

app.py
pages/
  1__Dashboard.py
  2__Settings.py

Streamlit auto-discovers them and builds a sidebar nav. Filenames become page titles (numeric prefix sets order, emoji becomes icon).

For new apps prefer the modern API — it gives you control over auth-gated pages, dynamic page lists, and grouped navigation.


2. When should you split into multiple pages vs. one page with tabs?

Use multiple pages when Use tabs when
Sections are independent and don’t share heavy state Sections are facets of the same data
You want bookmarkable URLs per page Single URL is fine
Each section is its own concern (admin, user, settings) All views relate to one entity
Heavy code per section — splitting helps readability Lightweight switching

Tabs (st.tabs) re-render together and don’t change the URL. Pages have separate URLs and only run the active page’s script.


3. How do you share state across pages?

st.session_state is shared across all pages within a session. No extra setup:

# home.py
st.session_state["user"] = login_form()

# dashboard.py
user = st.session_state.get("user")
if user is None:
    st.warning("Please log in on the Home page.")
    st.stop()

Cache decorators (@st.cache_data, @st.cache_resource) are also shared across pages — a model loaded on one page is available on all others.

What is not shared: local variables in each page’s script. Each page’s script runs from the top when navigated to.


4. How do you make pages conditional (auth-gated, role-based)?

With st.navigation, build the page list dynamically:

pages = [st.Page("home.py", title="Home")]

if st.session_state.get("logged_in"):
    pages += [
        st.Page("dashboard.py", title="Dashboard"),
        st.Page("settings.py", title="Settings"),
    ]
if st.session_state.get("is_admin"):
    pages += [st.Page("admin.py", title="Admin")]

st.navigation(pages).run()

This is the main reason to prefer st.navigation over the pages/ directory: you decide which pages exist per user.

For the legacy pages/ approach, gate at the top of each page:

if not st.session_state.get("is_admin"):
    st.error("Forbidden")
    st.stop()

5. How do page URLs and query parameters work?

# Read query params from URL like ?user=42&tab=stats
params = st.query_params
user_id = params.get("user")        # "42"

# Set / mutate
st.query_params["tab"] = "stats"
st.query_params.clear()             # remove all

# Build a deep link
url = f"/dashboard?user={user_id}"
st.link_button("Open dashboard", url)

st.query_params is dict-like. Setting a key updates the browser URL without reloading. Use it for bookmarkable filters, shareable links, and deep navigation.

Pages have URLs like /Dashboard (from page title). With st.Page, set url_path="dashboard" for control.


6. How do you switch pages programmatically?

if st.button("Go to dashboard"):
    st.switch_page("dashboard.py")           # legacy pages/
    # or with st.Page objects:
    st.switch_page(dashboard_page)

st.switch_page interrupts the current run and renders the target page. Useful after login: redirect to the home page once authenticated.


7. Where do you put shared code (utilities, models, DB)?

A normal Python module next to the entrypoint, imported from each page:

app.py
db.py             # connection helpers, queries
models.py         # pydantic models
auth.py           # login, role checks
pages/
  1_dashboard.py
  2_admin.py
# pages/1_dashboard.py
import streamlit as st
from db import get_engine
from auth import require_login

require_login()
engine = get_engine()
...

Cache resources (DB engines, models) at module level with @st.cache_resource so each page gets the same instance:

# db.py
import streamlit as st
from sqlalchemy import create_engine

@st.cache_resource
def get_engine():
    return create_engine(st.secrets["DATABASE_URL"])

8. How do you customize the sidebar / nav?

With st.navigation, group pages by section:

pages = {
    "Account": [
        st.Page("home.py", title="Home"),
        st.Page("profile.py", title="Profile"),
    ],
    "Reports": [
        st.Page("daily.py", title="Daily"),
        st.Page("monthly.py", title="Monthly"),
    ],
}
st.navigation(pages).run()

Position with st.navigation(pages, position="sidebar") (default) or position="hidden" (you build the nav yourself with buttons + st.switch_page).

Set the page-wide config once, in the entrypoint:

st.set_page_config(
    page_title="MyApp",
    page_icon=":material/dashboard:",
    layout="wide",
    initial_sidebar_state="expanded",
)

st.set_page_config must be the first Streamlit call in the script.


9. Common multipage pitfalls

Pitfall Fix
State lost when switching pages It isn’t — local vars reset, st.session_state persists. Check you’re using state, not locals
Cache cleared on navigation It isn’t. If you see this, something is calling .clear()
st.set_page_config errors on a sub-page Only call it once, in the entrypoint
Auth gate added on every page is duplicated Extract to auth.require_login() and call at the top of each page; or build a dynamic page list with st.navigation
URLs change between dev and prod due to title-based routing Set explicit url_path on each st.Page
Heavy imports rerun on every page nav Move them into cached factory functions; module-level imports run once per process per page module

Interview angle

  • “How do you build a multipage app?” - a pages/ directory for file-based routing, or st.navigation with st.Page for programmatic control, which is what you need to show pages conditionally based on role.
  • “Does state persist across pages?” - st.session_state does; local variables don’t. Shared setup should live in a cached resource so navigating doesn’t re-initialise it.
  • “How do you handle access control?” - not with hidden pages alone, since file-based routes are reachable directly. Check authorisation at the top of each page, or use programmatic navigation to build the page list per user.