Issue with asyncio run in streamlit

Hi guys !

I was trying to get something with a stream working in streamlit. ( no pun intended )
I tried a couple of approaches with threading and finished my search on asyncio.run with a small coroutine and for the most part it works really well. No need for any fancy code just a asyncio.run at the end. But there’s a small problem, I am seeing None printed at every run of loop.
Any idea what might be causing this ?

This is my script I am using to show a simple clock

import asyncio
import streamlit as st
from datetime import datetime

st.set_page_config(layout="wide")

st.markdown(
    """
    <style>
    .time {
        font-size: 130px !important;
        font-weight: 700 !important;
        color: #ec5953 !important;
    }
    </style>
    """,
    unsafe_allow_html=True
)

async def watch(test):
    while True:
        test.markdown(
            f"""
            <p class="time">
                {str(datetime.now())}
            </p>
            """, unsafe_allow_html=True)
        await asyncio.sleep(1)

test = st.empty()

if st.button("Click me."):
    st.image("https://cdn11.bigcommerce.com/s-7va6f0fjxr/images/stencil/1280x1280/products/40655/56894/Jdm-Decals-Like-A-Boss-Meme-Jdm-Decal-Sticker-Vinyl-Decal-Sticker__31547.1506197439.jpg?c=2", width=200)

asyncio.run(watch(test))

The output looks like this,

Thanks in advance ! :slight_smile:

6 Likes

Hey,

I’m pretty sure we got this problem before when building a realtime monitoring solution, but I don’t clearly remember I’d have to go back into old code :confused:

I recall we added a rerun somewhere and maybe an empty, like:

import asyncio
import streamlit as st
from datetime import datetime

st.set_page_config(layout="wide")

st.markdown(
    """
    <style>
    .time {
        font-size: 130px !important;
        font-weight: 700 !important;
        color: #ec5953 !important;
    }
    </style>
    """,
    unsafe_allow_html=True
)

async def watch(t: st._DeltaGenerator):
    while True:
        t.markdown(
            f"""
            <p class="time">
                {str(datetime.now())}
            </p>
            """, unsafe_allow_html=True)
        await asyncio.sleep(1)
        st.experimental_rerun()  # <-- here

test = st.empty()

asyncio.run(watch(test))

Maybe that’ll help you a bit :stuck_out_tongue:

3 Likes

Hey @andfanilo,

Thanks for the help!

Yep I thought of something similar but the issue is if you see my script I have some widgets after the empty and if I add a re-run they will keep refreshing as well :frowning:

Something like this,

Also is that old code public ? Can you point me to it ?
I guess the problem will be there only with st.button its working fine with a checkbox.

Thanks!

Aww yeah in my case it was the very last widget I remember :confused:

Unfortunately the code is buried into company internal code. I’ll have to dig into it…


While I will continue following your debugging for an asyncio solution (too much FastAPI recently, so getting to learn asyncio too :slight_smile: ), I think this has a solution using Threading and injecting the ReportContext info in the new thread here and here. Maybe we have a kinda similar issue with asyncio, where t has 2 references, one in the Streamlit thread and one in the asyncio thread, and one of them is lost and returns None… or the t.markdown() reruns the asyncio.run and this breaks things ? something like that… ?

sorry this is all brainstorming without testing and I’m writing a HDP tutorial at the same time :laughing:

@tim @thiago maybe you have some tips on this, how ReportContext, threads, the Tornado event loop and an external asyncio loop would all run together when t.markdown is called ?

Fanilo

The interesting part is that even if I dont write anything or dont use any streamlit widget it still writes None to the screen.

import asyncio


async def watch():
    while True:
        await asyncio.sleep(1)

asyncio.run(watch())

1 Like

:laughing: :thinking: :upside_down_face: :exploding_head:

(since there’s no reaction toolbar…)

@ash2shukla this is even better, well time to dig into what running a co-routines really means and if it tries to build a Streamlit container in the wrong place :slight_smile:

import asyncio
import streamlit as st

async def periodic():
    while True:
        st.write("Hello world")
        await asyncio.sleep(1)
        await asyncio.sleep(1)
        await asyncio.sleep(1)

asyncio.run(periodic())

image

1 Like

…so I guess the None is actually the result of await asyncio.sleep(0) which Streamlit naturally writes in Streamlit like when you just write "Hello World" in a Python script.

import asyncio
import streamlit as st

async def periodic():
    while True:
        st.write("Hello world")
        r = await asyncio.sleep(1)
        st.write(f"asyncio sleep ? {r}")

asyncio.run(periodic())

image

So back to your code

import asyncio
import streamlit as st
from datetime import datetime

st.set_page_config(layout="wide")

st.markdown(
    """
    <style>
    .time {
        font-size: 130px !important;
        font-weight: 700 !important;
        color: #ec5953 !important;
    }
    </style>
    """,
    unsafe_allow_html=True
)

async def watch(test):
    while True:
        test.markdown(
            f"""
            <p class="time">
                {str(datetime.now())}
            </p>
            """, unsafe_allow_html=True)
        r = await asyncio.sleep(1)

test = st.empty()

if st.button("Click me."):
    st.image("https://cdn11.bigcommerce.com/s-7va6f0fjxr/images/stencil/1280x1280/products/40655/56894/Jdm-Decals-Like-A-Boss-Meme-Jdm-Decal-Sticker-Vinyl-Decal-Sticker__31547.1506197439.jpg?c=2", width=200)

asyncio.run(watch(test))

And now we have almost perfect 1 second timing too!

8 Likes

Damn. Like a boss @andfanilo like a boss ! :laughing: :laughing:

Also I think this is probably the most neat way to get something realtime working in streamlit. Thanks again !

3 Likes

:laughing:

That may make creating realtime dashboards a bit easier :thinking:

2 Likes

A whole lot easier mate. :laughing:

2 Likes

Great minds coming to the same conclusion :slight_smile:

Dang now I badly want to try this for realtime monitoring! But I really need to finish this Hortonworks university tutorial, so I expect a full demo and a blogpost from you this weekend :stuck_out_tongue:

2 Likes

I am on it sir. :nerd_face:

2 Likes

Just looked into this issue.

From Python asyncio docs, the coroutine

asyncio.sleep( delay , result=None, *, loop=None)

has a result parameter which is passed in a callback (and then printed by Streamlit wrapper).

A quick work-around would be something like

asyncio.sleep(1.0, result = “”)

i.e., change None to the empty string (double quotes twice with no space).

Streamlit should have their wrapper ignore the callback but this is my work-around for now.

See Python docs: Coroutines and Tasks — Python 3.9.7 documentation

Would that apply to all use cases? Probably not… and it’s easy enough to assign the statement to a dummy variable (i.e. r = await asyncio.sleep(1)).

1 Like

Hi I Have the the same issue still with the latest version and does not go away with the solution here (r = await asyncio.sleep(1.0, result=None))

The fix has been merged to the main branch.
Hope it will be included in the next release.

1 Like

Hi, I’m wondering if you could assist with implementing your code for a timedelta countdown. The countdown works but reruns with every widget interaction. Thanks!

H = 11
M = 32
S = 32
async def watch(test):
    while True:
        	t2 = timedelta(hours=H, minutes=M, seconds=S)
        	while t2 > timedelta(hours=0, minutes=0, seconds=0):
            		t2 -= timedelta(seconds=1)            
		            test.markdown(
                		f"""
                        		<p class="time">
                            			{t2}
                        		</p>
                       		""", unsafe_allow_html=True)
            		r = await asyncio.sleep(1)
test = st.empty()
number_input = st.number_input('Test', key='Test')
asyncio.run(watch(test))

One way to do this is to protect your initialization with st.session_state. Something like…

if 'timer' not in st.session_state:
    st.session_state.timer = (11, 32, 32)
    
async def watch(test):
    while True:
        H, M, S = st.session_state.timer
        t2 = timedelta(hours=H, minutes=M, seconds=S)
...

Thank you. Will need to preserve state in cache.