Improving Streamlit App with Fragments

Streamlit is a Python library designed for creating data-driven applications quickly and easily that has recently been growing in popularity dramatically. According to Google Trends, more Google searches about Streamlit have been executed now than ever before in Streamlit’s history. Not only is it popular now, but the trend shows that Streamlit is becoming exponentially more used every year. With the immense growth in its popularity, there has never been a better time to start learning Streamlit. Luckily for developers who may have missed out until now, Streamlit’s API is intuitive and mostly well-documented. However, there are some things in Streamlit which can be difficult to understand. One of the most obvious examples is fragments. Fragments are a way for developers to define areas of a Streamlit program which reload independently from the rest of the page. This behavior is very powerful but can also be very confusing. This article will discuss how Streamlit works without fragments, how this behavior changes with the addition of fragments, and how a developer can start using fragments in their Streamlit apps.

Background

Let’s start by examining how Streamlit works without fragments. In a normal Streamlit app, all the python code you’ve written is executed sequentially, from top to bottom. When a user interacts with a widget on the page, or st.rerun() is called, the entire page is queued to be reloaded from top to bottom. If a widget is used to set a variable, that variable will not take its widget’s value until that variable is set in the rerun.  

http://moser-blog-fragment-example-1.streamlit.app/

Picture showing a line going down over Streamlit code, to show how Streamlit code is usually run.

Motivation

Streamlit’s default behavior works well in most cases. However, there are some situations where rerunning the entire page for every update to a widget can be too slow or inefficient for the program’s purpose. Here are some examples of things in Streamlit that are difficult to create or work with because of the default behavior.  

Dynamic Forms

In Streamlit, forms batch together widget inputs to sync with a single button press. This provides a way to let users enter a series of entries without triggering several page reruns, saving both the user’s time and processing power. However, forms are not dynamic; the values stored in the widgets before the form submit button is pressed cannot be read. This means that it is impossible to have live input validation, such as locking the submit button until all required fields in the form are filled out.

http://moser-blog-app-example-dynamic-form.streamlit.app/

A Streamlit program which shows two forms: one with a static form that only validates user input after it has been submitted, leading to a poor user experience, and one with a form that dynamically enables the submit button when the user’s form input is valid, leading to a better user experience

Graphics

            Graphics are a great way to display information, and Streamlit provides many ways to create various graphs and charts to explain your data. However, generating multiple graphs per page run can be quite taxing and can dramatically slow down the app. It is possible to cache graphs to only generate them once, but in situations where the data being displayed could change, or where the graph needs to adapt to user input, caching will not solve the problem.

Data Streaming

            Sometimes, apps require data to be constantly fetched to give users the most up-to-date information possible. Streamlit provides a way to cache database results for a set amount of time, but there is no method of forcing the page to use these updated values automatically; a page rerun must be triggered to update the data. A potential solution is to use time.sleep() to trigger reruns on fixed intervals, but this will cause the entire page to rerun just to refresh data. It would be great if we could scope this rerun to just affect the data that needs reloaded, but with default Streamlit behavior, it seems impossible.

Enter Fragments

Fragments change Streamlit’s default behavior by defining specific functions which can be rerun independently from the rest of the page. This means that sections of an app which need to constantly reflect new data may be reloaded at a fixed rate, or sections of an app which require lots of user input may be separated from the rest of the page to increase load time. While fragments have many benefits, they can also lead to odd behavior if not properly configured. Let’s examine how Streamlit will deal with user inputs in different areas of a program with fragments.

Inside Fragment

When a rerun is triggered from inside a fragment, the fragment is queued to be rerun. Any normal Python variables defined inside the fragment are reset, and session state variables are updated. The widgets outside the fragment will not be updated, even if session state variables assigning their properties are changed.

Outside Fragment

When a rerun is triggered from outside of a fragment, the entire page is rerun as normal. All python variables are reset and the session state variables have their values updated. A fragment is not protected from updates in the rest of the page. A fragment only protects the rest of the page from updates within itself.  

http://moser-blog-fragment-example-3.streamlit.app/

A streamlit app showing how triggering reruns in different sections of a streamlit app containing fragments effects the rest of the app.

Defining a Fragment

You can define a fragment in your code by using the decorator @st.experimental_fragment. This decorator takes one parameter: run_every. This parameter defaults to None, and defines the time between automatic reloads for a fragment. This is used for streaming data from a database, API, or some other data storage. “run_every” can be a Int or Float representing the number of seconds between reruns, a String that can be converted to a Timedelta object with Pandas such as “2h” or “3 days”, or a Timedelta object. When run_every is none, the fragment will never automatically refresh; only a user input or a st.rerun() call will refresh the fragment.

@st.experimental_fragment(run_every=5)
def random_number_fragment():
    st.write("This code will be rerun every 5 seconds!")

    st.button("Click to manualy trigger a reload!")

    st.metric("Latest random number", random.randint(0, 99))

random_number_fragment()

Conclusion

By now, you should have a clearer grasp of how fragments operate within Streamlit. We've walked through how Streamlit works without fragments, how adding fragments alters its behavior, and how to define fragments within your Streamlit projects. Armed with this knowledge, you're now better equipped to elevate your Streamlit skills and craft captivating, interactive applications. Whether you're a seasoned developer or just starting out, integrating fragments into your Streamlit toolkit opens up exciting possibilities for creating dynamic and engaging user experiences. So, go ahead, experiment, and bring your ideas to life with Streamlit's versatile capabilities!

By now, it should be clear that fragments allow developers to supercharge Streamlit’s normal capabilities and and create more complex applications. Without fragments, complex Streamlit apps can run into problems and become slow or unworkable. Fragments allow these advanced programs to be developed quicker, run faster, and perform better than would otherwise be possible. While fragments can lead to unexpected behavior with Session States at first, the interaction is easy to understand at a second glance. Fragments enable developers to harness Streamlit's full potential, ensuring developers’ applications achieve optimal performance and functionality.

Update: 7/29/2024

With the release of Streamlit 1.37 on July 25, 2024, fragments have changed and are no longer an experimental feature. As such, the syntax to define a fragment has changed.

Old syntax

@st.experimental_fragment(run_every="4s")
def this_is_a_fragment():
    st.write("This is inside of a fragment!")

New syntax

@st.fragment(run_every="4s")
def this_is_a_fragment():
    st.write("This is inside of a fragment!")

The greatest change is that the decorator “@st.experimental_fragment” has been replaced with “@st.fragment”. The old experimental decorator is deprecated and will be removed on January 1, 2025. Another important change is that “st.rerun” can now be scoped to only rerun a fragment it is inside by setting the keyword parameter “scope” to “fragment”. This will make it much simpler to rerun fragments without rerunning the entire page.

st.rerun(scope="fragment") # This will only rerun the fragment it is inside
st.rerun() # This will rerun the entire app

Fragments still have the same functionality as when this article was written. The background and purpose of fragments have not changed. Fragments are even more useful now than they were before.

Links to Other Moser Webpages

http://7di.d809.com/blog/creating-a-web-app-with-snowflake-and-streamlit

References

Include any links from sources outside the Moser website that are referenced in the article:

http://trends.google.com/trends/explore?date=all&q=streamlit&hl=en

http://docs.streamlit.io/develop/api-reference/execution-flow/st.fragment

http://docs.streamlit.io/develop/concepts/architecture/fragments

Contact Moser today for your company’s technology needs.

Ethan Poorbaugh

Ethan Poorbaugh is a Data Engineering Intern at Moser Consulting. His attends Anderson University where he majors in Data Science. Ethan has experience in big data technologies such as PostgreSQL, Snowflake, and Python and is ready to gain more experience in the field.

Previous
Previous

Using AI to Transform Every Team in Your Organization

Next
Next

The Crucial Role of Data Visualization