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!

10 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.

I assume displaying a simple running time is the goal here. Hope it helps someone.

I found the st.fragment the preferred short and clean way in my humble opinion. After all, the running time can run in its own fragment space independently.

Something like this?

import streamlit as st
from datetime import datetime, date
import time

@st.fragment(run_every=1)
def clock_counter():
    now = datetime.now()
    current_time = now.strftime("%H:%M:%S")
    st.write(f"Current time: {current_time}")

clock_counter()

Caveats: The above running time will stop if another ‘live’ resource like the st.progress is running or until freed. And it may interrupt with other effects like snow and balloons.

Note: The above solution async solution is giving an error.