Dynamically Generate Multipage Streamlit Apps — Golden Formulas App Part 2

José Fernando Costa
6 min readJan 21, 2025

--

Back in October I published a walkthrough of my latest Streamlit app that consumed Notion information and made it publicly available then and there.

However, from a technical perspective I was not satisfied. The goal to begin with would be for me to write the information in Notion and have it display automatically in the app. But that only worked for existing pages. What if I renamed pages? Or added new pages for new topics? I would have to manually commit the scripts to generate those pages…

I went back and finished the job: the app now generates the individual pages on the fly based on the information loaded from Notion. In other words, the information displayed in the app mirrors what exists in Notion in real time.

I will show you how I did it for my Golden Formulas app and also leave you with a separate live demo for other use cases.

Image generated with Imagen 3

Changes to the Golden Formula App

Bulk of the changes boils down to a new utils.py script. This file has a single function manage_subpages:

  • Load existing pages from Notion via API
  • Read existing local subpages
  • Subpages that exist in Notion but not locally are created (their scripts are created)
  • Subpages that exist locally but not in Notion are deleted (their scripts are deleted)
def manage_subpages():
"""
Create new subpages on the app based on which Golden Formulas pages exist in Notion.
And delete subpages in the app for pages that no longer exist in Notion.
"""
# Check for new pages to be created
# Which pages exist at source
notion_api = NotionApi()
pages_available = list(notion_api.available_child_pages.keys())

# Which pages exist locally
local_pages_files = os.listdir(os.path.join(os.getcwd(), "pages"))
local_pages = [page.replace(".py", "") for page in local_pages_files if page != ".gitkeep"]

# Which pages to be created
missing_pages = [page for page in pages_available if page not in local_pages]
for page in missing_pages:
subpage_code = """
import streamlit as st
from classes.SubPage import SubPage
import os

# Get page name from script name
page_name = os.path.basename(__file__).replace(".py", "")

# Start class to handle Notion connectivity and content formatting
sub_page = SubPage(page_name)
# Tidy up content for page
display_string = sub_page.get_formatted_bullet_list()

# Streamlit page config
st.set_page_config(
page_title = sub_page.get_page_name()
)

# Page header
st.markdown(f"# {sub_page.get_page_name()}")

# Page body
st.markdown(display_string)"""

subpage_name = f"{page}.py"
out_path = os.path.join(os.getcwd(), "pages", subpage_name)
with open(out_path, "w") as f:
print(f"Created page {subpage_name}")
f.write(subpage_code)

todelete_pages = [page for page in local_pages if page not in pages_available]
for page in todelete_pages:
subpage_name = f"{page}.py"
out_path = os.path.join(os.getcwd(), "pages", subpage_name)
print(f"Deleted page {subpage_name}")
os.remove(out_path)

Nothing fancy here. My existing code for each subpage was already identical for all, so I copied into this function as a string.

When the code detects a new page to be created then it creates the corresponding script using that template code.

A standard file operation to write a new file. Deletion mirrors that process except it deletes the file.

Now whenever a user lands on the Home page it triggers this manage_subpages function to check for subpages to be created or deleted. That’s it, happy days :)

One last note from my app development: while running the app locally, I could see the subpage scripts generated, but the app running in localhost could never pick up the new files. However, I tried pushing the code into the live app (hosted in Streamlit community cloud), and it works seamlessly, doesn’t even require a refresh after the scripts are created.

Dynamic Multipage Demo App

(sample demo live here and its GitHub repository here)

For the purpose of this post I then worked with Claude AI to generate a new sample app to demo dynamic multipage functionality. I did not provide it any of my code, only asked for the subpages to be dynamically generated and allow users to delete subpages.

Claude generated a sample around a data analysis app with synthetic data. The dynamic part is for allowing users to generate different dashboards, slicing the datasets by different dimensions.

The pre-built pages are:

  • Home: the landing page
  • Data Analysis: the hub page for managing other pages
  • Visualization: the only report page that exists out of the box

Users can generate three extra pages where they can analyse datasets sliced by their dimensions:

  • Sales
  • Marketing
  • Customer

Users can control existing additional subpages from the Data Analysis page.

The Data Analysis page

Not only can they create the pages

New subpage created

But also delete them

Prompt for deleting subpage

There is not a notification for deletion but the entry in this table and on the menu navigation both disappear immediately.

I won’t walk you through the entire codebase for this demo especially because there is a lot of data generation and plotting which are not the point of this post to begin with.

But if we have a look at the logic to handle page creation/deletion for comparison with my approach (pages/1_📊_data_analysis.py)

if 'delete_confirmations' not in st.session_state:
st.session_state.delete_confirmations = {}

# List and manage existing generated pages
generated_dir = Path("pages")
if generated_dir.exists():
existing_pages = list(generated_dir.glob("*.py"))
# Don't consider the pre-built pages for listing
existing_pages = [page for page in existing_pages if page.stem[0] not in ("1", "2")]

if existing_pages:
st.subheader("Manage Analysis Pages")

# Create a container for the list
pages_container = st.container()

# Create columns for each page
for page in existing_pages:
page_id = page.stem

# Initialize this page's confirmation state if not exists
if page_id not in st.session_state.delete_confirmations:
st.session_state.delete_confirmations[page_id] = False

col1, col2, col3 = st.columns([3, 1, 1])

with col1:
st.write(f"📊 {page_id}")

with col2:
# Extract category from filename
category = page_id.split('_')[-2] if '_' in page_id else 'Unknown'
st.caption(f"Category: {category}")

with col3:
if not st.session_state.delete_confirmations[page_id]:
# Show delete button first
if st.button("🗑️ Delete", key=f"delete_{page_id}"):
st.session_state.delete_confirmations[page_id] = True
st.rerun()
else:
# Show confirm/cancel buttons
c1, c2 = st.columns(2)
with c1:
if st.button("✅ Confirm", key=f"confirm_{page_id}"):
try:
page.unlink() # Delete the file
st.success(f"Deleted {page_id}")
# Reset confirmation state
st.session_state.delete_confirmations[page_id] = False
# Rerun to update the page list
st.rerun()
except Exception as e:
st.error(f"Error deleting page: {str(e)}")
with c2:
if st.button("❌ Cancel", key=f"cancel_{page_id}"):
st.session_state.delete_confirmations[page_id] = False
st.rerun()

# Add a separator line
st.markdown("---")
else:
st.info("No analysis pages generated yet. Use the form above to create your first analysis page.")

Let’s focus on that with col3 code block (Claude generated a with block for each column in that subpage listing table):

  • The user session will have a dictionary that maps the subpage id to a Boolean flag: True if the page is to be deleted, False by default
  • If the user is first landing in the page then the dictionary is an empty dictionary (this is right at the top of the code snippet)
  • When generating the table visual, the code lookups all subpages, lists them, and adds them to the user state dictionary with the default False value
  • When the user clicks the delete button, they are prompted for confirmation
  • If the user cancels deletion, then nothing happens
  • If the user confirms deletion, then the flag flips to True and the file is physically deleted

While I don’t particularly like that table formatting, I was quite pleased with Claude using a Path object from the pathlib module to manage local files.

I had also never dealt with state management in Streamlit so it was a great way to see it in action first-hand.

Closing Thoughts

Overall both mine and Claude’s approach achieve the same end result — the difference here was in the requirement.

I want my pages to mirror whatever gets extracted from the API without approval.

The demo is for a completely different use case. Users can generate their pages on demand. On top of that, they can choose to keep the pages, or delete them then and there.

Ultimately, the two approaches in this post will cover multiple scenarios where you can dynamically manage subpages in your app.

--

--

José Fernando Costa
José Fernando Costa

Written by José Fernando Costa

Documenting my life in text form for various audiences

No responses yet