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
- We can achieve similar results using
streamlit.web.bootstrap.run
, but it is more of an internal API, so here I userunpy
instead. - Since the module
my_package.app
is technically run as a script when you callrunpy.run_module()
, if you need to import from any other modules in the package, you need to use absolute import. In the above example, inapp.py
, if we instead importmy_func
usingfrom .my_module import my_func
, it won’t work. This is becauseapp.py
is being called as a script and does not know that it belongs tomy_package
. - If you have a multipage app, make sure the
pages
directory is also installed. Here, I am using hatchling, so as long as theonly-packages
option is not set totrue
thepages
directory will be installed. - 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.