St.button behavior

Hi

I am trying to use st.button but its behavior is puzzling me. Check the code below:

import streamlit as st

if "run_state" not in st.session_state:
    st.session_state.run_state = False

def disable(b):
    st.session_state["run_state"] = b
    print("Disabled run_state",st.session_state.run_state)

if st.button("Say hello", disabled=st.session_state.run_state, on_click=disable, args=(True,)):
    st.write("Why hello there")

else:
    st.write("Goodbye")

if st.button("Reset Button" ):
    disable(False)

When I execute this code and click the “Say hello” button, the button is disabled due to the on_click function.
However, when I click the “Reset Button”, the st.session_state.run_state value changes to False, but the “Say hello” button still remains disabled. I need to click the “Reset Button” twice to enable the “Say hello” button.

Why is it behaving this way? I don’t want to click the reset button twice.
Am I doing something wrong, or is this the expected behavior.
Please explain.

Thanks.

Welcome to the Streamlit community, and thanks for your thoughtful question! :balloon: What you’re seeing is a common gotcha with Streamlit’s session state and widget rendering order: when you update st.session_state.run_state after the “Say hello” button is already rendered, the change doesn’t affect the current run—only the next one. That’s why you need to click “Reset Button” twice: the first click updates the state, and the second rerun actually enables the button.

To fix this, update the session state before rendering the button, or use a callback with st.button that sets the state and triggers a rerun. For example, you can use st.rerun() after resetting the state so the UI reflects the change immediately. See the Streamlit docs on button/session state behavior and forum discussions for more details.

Here’s a pattern that works:

import streamlit as st

if "run_state" not in st.session_state:
    st.session_state.run_state = False

def disable(b):
    st.session_state["run_state"] = b

def reset():
    st.session_state["run_state"] = False
    st.rerun()

if st.button("Say hello", disabled=st.session_state.run_state, on_click=disable, args=(True,)):
    st.write("Why hello there")
else:
    st.write("Goodbye")

st.button("Reset Button", on_click=reset)

Sources:

When I run this code, I get the following message:

Calling st.rerun() within a callback is a no-op.

This code does not work in streamlit version 1.61 and above.
When the button “say hello” is clicked, it disables but does not return a True value.
If used without the disabled parameter, the button works.

It does however work in version 1.55 but I am not sure till which number.
I think this is a bug.

Can you please fix?

Thanks