Build a nice CLI for your Streamlit apps with Click or Typer

February 17, 2022 | categories: Python, Programming | View Comments

https://danielnouri.org/media/build-a-nice-cli-for-your-streamlit-apps-with-click-or-typer.jpg

If you're like me, you enjoy creating small apps, demos, and doing exploratory data science work with Streamlit.

Here's a minimal Streamlit app that displays the contents of an Excel file in the browser:

import pandas
import streamlit as st

def app():
    st.title("My awesome streamlit app")
    df = pandas.read_excel('myexcelfile.xlsx')
    st.table(df)

if __name__ == '__main__':
    app()

You can run your app like this and then view it in your browser:

$ streamlit run myapp.py

Note

By the way. One of my favourite Streamlit features is that you can invoke these apps like normal Python programs as well (just call python myapp.py), which will run through the program, use default arguments for interactive elements everywhere, and exit, without starting up a browser or anything. It's a great way of quickly testing your app without hassle!

Back to our app; what if we wanted to pass the filename of the Excel file on the command line instead of hardcoding it?

We could do that without the help of any library, just passing any arguments from sys.argv to our app function like so:

import sys
import pandas
import streamlit as st

def app(excel_filename='myexcelfile.xlsx'):
    st.title("My awesome streamlit app")
    df = pandas.read_excel(excel_filename)
    st.table(df)

if __name__ == '__main__':
    app(*sys.argv[1:])

Note that excel_filename still has a default so it'll continue to work without any arguments. However, we can now pass a different filename on the command line:

$ streamlit run myapp.py -- myotherfile.xlsx

Note the double dashes (--) that separate the arguments to our own script (stuff that'll end up in sys.argv[1:] above) from any arguments that the streamlit command itself expects (such as the run and myapp.py; find these using streamlit --help etc.).

Now you might say that this approach with passing sys.argv[1:] has its limits and isn't exactly user friendly, and you'd be right: If we wanted to add another argument, we'd have to pass them in the right order always, our way of supporting defaults would breaks down, there's no automatic type conversions, and so on.

So let's use a proper command line library instead, like Click:

import click
import pandas
import streamlit as st

@click.command()
@click.option('--excel-filename', default='myexcelfile.xlsx')
def app(excel_filename):
    st.title("My awesome streamlit app")
    df = pandas.read_excel(excel_filename)
    st.table(df)

if __name__ == '__main__':
    app()  # this will break, see below

Unfortunately, this approach breaks with SystemExit: 0 when we run this using streamlit run. The trick is to tell Click that we're not running in standalone_mode. So to fix the problem, we simply replace the last two lines of our program with this:

if __name__ == '__main__':
    app(standalone_mode=False)

And now we have a proper CLI for our Streamlit app which allows to pass options and many more things. We'll remember to use the -- delimiter to distinguish between arguments to the streamlit command itself and arguments to our app:

$ streamlit run myapp.py  # run with default --excel-filename
$ streamlit run myapp.py -- --excel-filename=myotherfile.xlsx

If you prefer Typer over Click, you have to pretty much do the same thing:

import pandas
import streamlit as st
import typer

cli = typer.Typer()

@cli.command()
def app(excel_filename: str = 'myexcelfile.xlsx'):
    st.title("My awesome streamlit app")
    df = pandas.read_excel(excel_filename)
    st.table(df)

if __name__ == '__main__':
    cli(standalone_mode=False)