Installable Streamlit App

The open-source Python framework Streamlit has made building web apps in python fast and easy. It is especially useful in building data apps and generative AI chat applications.

However, if you are someone like me who likes to have their python code packaged with pip, you may find that it is not very straight forward. Streamlit is designed to run python scripts. You start your app by running

streamlit run app.py

or

python -m streamlit run app.py

assuming app.py is your app.

Often as a project grows larger, we need to organize the code base into different modules. For example, a data processing app may have a backend submodule called data_prep and the web app for displaying the processed data may be in another module called app. It would be ideal if we can package a streamlit web app with our package. Below I will show how to achieve this.

Project structure

Below is an example project structure for a streamlit app packaged with other modules. The approach I am using here is a modified version from the disccusion of this issue.

project-root/
├── pyproject.toml
├── README.md
├── my_package/
│   ├── __init__.py
│   ├── my_module.py
│   ├── app.py
│   └── pages/
│       ├── page_1.py
│       └── page_2.py

The project has a top level package my_package with a few submodules. The subdirectory pages in not a package, but it contains other pages.

Build configuration

Let’s first look at the pyproject.toml file.

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "installable-streamlit-app"
version = "1.0"
dependencies = [
  "streamlit",
]
requires-python = ">=3.11"

[project.scripts]
run-my-app = "my_package.app:run_app"

[tool.hatch.build.targets.wheel]
packages = ["my_package"]

Here, I am using hatchling as our build backend, but you can use any other build backends. The most important part is the [project.scripts] section. Here, we set up a command run-my-app, which calls the function run_app in my_package.app.

The web app

Below is the web app script packaged as my_package.app.

import os
import sys
import runpy

import streamlit as st

from my_package.my_module import my_func
# from .my_module import my_func  # Won't work


def main():
    st.title('Main Page')
    st.write('Hello Main Page!')
    st.write(my_func())


def run_app():
    script_path = os.path.abspath(__file__)
    sys.argv = ['streamlit', 'run', script_path] + sys.argv[1:]
    runpy.run_module('streamlit', run_name='__main__')


if __name__ == '__main__':
    main()

The main() function is just a simple streamlit app. The trick is in the run_app() function. Here, we are using runpy.run_module() to run the module streamlit. Before we run the streamlit module, we modify sys.argv to take the script (this module) as argument so that it is passed to streamlit run. Note that here I am adding the original sys.argv[1:] to sys.argv so that you can pass extra command line arguments to streamlit run. The run_name argument sets the __name__ to __main__, so our main function will be called when we run runpy.run_module().

That it! Now if you just install this package with

pip install .

you can just start your app using

run-my-app

You can also pass in extra arguments when starting you app. For example,

run-my-app --server.port 5555

will start your app with port 5555 instead of the default 8501.

Notes and caveats

  1. We can achieve similar results using streamlit.web.bootstrap.run, but it is more of an internal API, so here I use runpy instead.
  2. Since the module my_package.app is technically run as a script when you call runpy.run_module(), if you need to import from any other modules in the package, you need to use absolute import. In the above example, in app.py, if we instead import my_func using from .my_module import my_func, it won’t work. This is because app.py is being called as a script and does not know that it belongs to my_package.
  3. If you have a multipage app, make sure the pages directory is also installed. Here, I am using hatchling, so as long as the only-packages option is not set to true the pages directory will be installed.
  4. Pages under the pages directory also need to use absolute import when importing from this package.

Full example

The full example can be found here.