Buttons inside a for loop

I’m trying to write a simple app where we cycle a list of animals, and we ask the user if they like the animal or not.

The current app is here. You can check the code below:

animals = ['cat', 'dog', 'fish', 'turtle', 'hare', 'hamster']

liked_animals = []
disliked_animals = []

for animal in animals:
    st.subheader(f"Do you like {animal}?")

    yes_key = f"yes_{animal}"
    no_key = f"no_{animal}"

    yes_button = st.button("YES", key=yes_key)
    no_button = st.button("NO", key=no_key)

    while not yes_button and not no_button:
        pass

    if yes_button:
        liked_animals.append(animal)
    elif no_button:
        disliked_animals.append(animal)

Unfortunately, the app gets stuck in the second interaction (with “dog”), though no error is thrown. Moreover, I’d like to “overwrite” the text and the buttons of the previous interactions (so when asking about dogs, the user doesn’t see the previous question about cats).

Welcome.

Be sure you read the main concept of streamlit especially on Data Flow.

You are using buttons, you need to read the button behavior as well.

Also read this forum guide and tips.

Regarding your code, button plus loop is going to be difficult to debug. When users press that button, streamlit will rerun the code from top to bottom. We need to rethink the flow of your code.

In this page there is a search button located at the top right.

image

Use this button to search for similar issues that you have. I saw a component/library/app that supports Q&A type app. It might give a hint to help with your issue.

A key part of how streamlit works is that all the code is re-run each time a user interacts with a widget. For example, if your user presses ‘YES’ for ‘cat’, the script re-runs, appends ‘cat’ to liked_animals and continues to ‘dog’ in the loop. If your user now presses ‘YES’ for ‘dog’, the script re-runs, creates a new empty liked_animals list and appends ‘cat’ to it and continues to ‘dog’.

An easy way around this would be to replace the buttons with widgets that retain their state between re-runs (like st.radio or st.checkbox) and include every question on the page.

If you really like the buttons and the design of prompting the user with each question, you will need to store the user’s selection in st.session_state so that the value is retained between re-runs. It’s a bit complicated because this isn’t really the sort of user interaction streamlit is geared toward. Here’s one way to do it:

animals = ['cat', 'dog', 'fish', 'turtle', 'hare', 'hamster']

#initialize lists in session_state
if 'liked_animals' not in st.session_state:
    st.session_state['liked_animals'] = []
if 'disliked_animals' not in st.session_state:
    st.session_state['disliked_animals'] = []


for animal in animals:
    #Check if animal already in liked_animals or disliked_animals
    if animal in st.session_state['liked_animals']:
        continue
    if animal in st.session_state['disliked_animals']:
        continue

    #Append animal from previous run to liked or disliked list
    try:
        if st.session_state['yes_button']:
            st.session_state['liked_animals'].append(animal)
            del st.session_state['yes_button']
            continue      
        if st.session_state['no_button']:
            st.session_state['disliked_animals'].append(animal)
            del st.session_state['no_button']
            continue

    #On first run neither yes_button or no_button are set
    except KeyError:
        pass

    st.subheader(f"Do you like {animal}?")

    st.button("YES",key='yes_button')
    st.button("NO",key='no_button')
    break

st.write(st.session_state['liked_animals'])
st.write(st.session_state['disliked_animals'])

I tried to implement your app, see some comments.

main.py

import streamlit as st
from streamlit import session_state as ss


animals = ['cat', 'dog', 'fish', 'turtle', 'hare', 'hamster']


# Declare some session variables. It is like a global variable
# in python script. It is called session variables because once
# the browser is reset, their values will go back to its initial
# values. We also do this because streamlit reruns the
# code from top to bottom if there are changes to the states
# or an explicit "st.rerun()" command is called.
# By doing this, their values will not be overwritten as you update
# while streamlit reruns the code from top to bottom on your code.
if 'a_index' not in ss:
    ss.a_index = 0

if 'liked_animals' not in ss:
    ss.liked_animals = []

if 'disliked_animals' not in ss:
    ss.disliked_animals = []

# Once all the amimals are shown.
if 'done' not in ss:
    ss.done = False


def update_cb(animal):
    """A callback function to update the answer.
    
    Whenever a button is used, try the callback method as much as possible.
    """

    # That ans is the key we assign in radio button.
    if ss.ans == 'YES':
        ss.liked_animals.append(animal)
    else:
        ss.disliked_animals.append(animal)

    ss.a_index += 1  # load next animal


def restart_cb():
    """Enable the answer form button again."""
    ss.done = False


if ss.a_index > len(animals) - 1:
    animal = animals[0]  # avoid index overflow
    ss.done = True
else:
    animal = animals[ss.a_index]

# The best widget to use when dealing with user input is the form.
with st.form('form_k'):
    st.subheader(f"Do you like {animal}?")
    st.radio('Select', ['YES', 'NO'], horizontal=True, key='ans')
    st.form_submit_button('Submit', on_click=update_cb, disabled=ss.done, args=(animal,))

# Report after exhausting the number of animals
if ss.a_index > len(animals) - 1:
    st.write('### Report')

    with st.container(border=True):
        st.markdown(f'''number of **YES**: {len(ss.liked_animals)}  
            animals: {ss.liked_animals}
        ''')
        st.markdown(f'''number of **NO**: {len(ss.disliked_animals)}  
            animals: {ss.disliked_animals}
        ''')

    # Reset variables values ready for next round.
    ss.a_index = 0
    ss.liked_animals = []
    ss.disliked_animals = []

    # Build a button for next round.
    st.button(':+1: Next round', on_click=restart_cb)

Sample Output

This should do what you need without having to loop through.

import streamlit as st

animals = ["cat", "dog", "fish", "turtle", "hare", "hamster"]


def add_animal(animal: str, add_to: str) -> None:
    st.session_state[add_to].append(animal)
    st.session_state["current_animal_index"] += 1


if "liked_animals" not in st.session_state:
    st.session_state["liked_animals"] = []
if "disliked_animals" not in st.session_state:
    st.session_state["disliked_animals"] = []
if "current_animal_index" not in st.session_state:
    st.session_state["current_animal_index"] = 0

question_placeholder = st.empty()
if st.session_state["current_animal_index"] != len(animals):
    animal = animals[st.session_state["current_animal_index"]]
    with question_placeholder.container():
        st.subheader(f"Do you like {animal}?")
        yes_button = st.button(
            "YES",
            on_click=add_animal,
            args=[animal, "liked_animals"],
            key=f"{animal}_yes",
        )
        no_button = st.button(
            "NO",
            on_click=add_animal,
            args=[
                animal,
                "disliked_animals",
            ],
            key=f"{animal}_no",
        )
else:
    st.markdown(
        f"""
        ## Results
        |Yes | No|
        |:---:|:---:|
        |{','.join(st.session_state["liked_animals"])}|{','.join(st.session_state["disliked_animals"])}
    """
    )

import streamlit as st

animals = [‘cat’, ‘dog’, ‘fish’, ‘turtle’, ‘hare’, ‘hamster’]

liked_animals =
disliked_animals =

for animal in animals:
# Create an empty space to dynamically update content
content_placeholder = st.empty()

# Display the question and buttons
content_placeholder.subheader(f"Do you like {animal}?")
yes_button = content_placeholder.button("YES")
no_button = content_placeholder.button("NO")

# Wait for user input
if yes_button:
    liked_animals.append(animal)
elif no_button:
    disliked_animals.append(animal)

# Clear the content for the next iteration
content_placeholder.empty()

Try this code. Hope this will resolve the issue.

Did this code helps you?