Creating a basic quiz app

Hi,

I am trying to create a basic quiz app, but it is not working. The choices are accepted without confirming.
Here is a stripped-down version of what I am doing:

import streamlit as st
import numpy as np

arr = np.random.randint(0, 100, 2)
q = f"{arr[0]} * {arr[1]}"
ans = arr[0]*arr[1]
choices = [0, ans, ans-1, ans+1, ans+2]

st.text(f"Solve: {q}")
a = st.selectbox('Answer:', choices)
st.write(f"You chose {a}")

if (ans == int(a)):
    st.write("Correct!")
else:
    st.write(f"Wrong!, the correct answer is {ans}")
            
st.button('Rerun')

The answer gets accepted before a selection is made, and after selecting the app advances to the next question. I have played around with adding more buttons and also tried the SessionState hack but can’t get this to work.

Possible missing something basic.

Appreciate any help!

Hi areo,

The problem is, that once you select an option, the script is rerun from the top (that is how streamlit works), which means a new array of choices is made, prompting creation of a new selectbox widget.

I think there are various different ways to make an app like yours work, I am not sure my solution is the most elegant, but it uses some concepts I think you may find useful.

It relies on caching and use of the session_state gist, see below

In addition, I had to make use of a rerun exception, which just prompts a rerun of the script from the top…

import streamlit as st
import numpy as np
import session_state
from streamlit.ScriptRunner import RerunException
from streamlit.ScriptRequestQueue import RerunData

state = session_state.get(question_number=0)

@st.cache
def get_question(question_number):
    # np.random.seed(question_number) - could consider adding this
    arr = np.random.randint(1, 100, 2)
    q = f"{arr[0]} * {arr[1]}"
    ans = arr[0]*arr[1]
    choices = [0, ans, ans-1, ans+1, ans+2]
    return arr, q, ans, choices

arr, q, ans, choices = get_question(state.question_number)

st.text(f"Solve: {q}")
a = int(st.selectbox('Answer:', choices))

if a != 0:
    st.write(f"You chose {a}")
    if (ans == a):
        st.write("Correct!")
    else:
        st.write(f"Wrong!, the correct answer is {ans}")
            
if st.button('Rerun'):
    state.question_number += 1
    raise RerunException(RerunData(widget_state=None))

And in a seperate file in the same folder named session_state.py:

"""Hack to add per-session state to Streamlit.

Usage
-----

>>> import SessionState
>>>
>>> session_state = SessionState.get(user_name='', favorite_color='black')
>>> session_state.user_name
''
>>> session_state.user_name = 'Mary'
>>> session_state.favorite_color
'black'

Since you set user_name above, next time your script runs this will be the
result:
>>> session_state = get(user_name='', favorite_color='black')
>>> session_state.user_name
'Mary'

"""
import streamlit.ReportThread as ReportThread
from streamlit.server.Server import Server


class SessionState(object):
    def __init__(self, **kwargs):
        """A new SessionState object.

        Parameters
        ----------
        **kwargs : any
            Default values for the session state.

        Example
        -------
        >>> session_state = SessionState(user_name='', favorite_color='black')
        >>> session_state.user_name = 'Mary'
        ''
        >>> session_state.favorite_color
        'black'

        """
        for key, val in kwargs.items():
            setattr(self, key, val)


def get(**kwargs):
    """Gets a SessionState object for the current session.

    Creates a new object if necessary.

    Parameters
    ----------
    **kwargs : any
        Default values you want to add to the session state, if we're creating a
        new one.

    Example
    -------
    >>> session_state = get(user_name='', favorite_color='black')
    >>> session_state.user_name
    ''
    >>> session_state.user_name = 'Mary'
    >>> session_state.favorite_color
    'black'

    Since you set user_name above, next time your script runs this will be the
    result:
    >>> session_state = get(user_name='', favorite_color='black')
    >>> session_state.user_name
    'Mary'

    """
    # Hack to get the session object from Streamlit.

    ctx = ReportThread.get_report_ctx()

    this_session = None

    current_server = Server.get_current()
    if hasattr(current_server, "_session_infos"):
        # Streamlit < 0.56
        session_infos = Server.get_current()._session_infos.values()
    else:
        session_infos = Server.get_current()._session_info_by_id.values()

    for session_info in session_infos:
        s = session_info.session
        if (
            # Streamlit < 0.54.0
            (hasattr(s, "_main_dg") and s._main_dg == ctx.main_dg)
            or
            # Streamlit >= 0.54.0
            (not hasattr(s, "_main_dg") and s.enqueue == ctx.enqueue)
        ):
            this_session = s

    if this_session is None:
        raise RuntimeError(
            "Oh noes. Couldn't get your Streamlit Session object"
            "Are you doing something fancy with threads?"
        )

    # Got the session object! Now let's attach some state into it.

    if not hasattr(this_session, "_custom_session_state"):
        this_session._custom_session_state = SessionState(**kwargs)

    return this_session._custom_session_state

Hope this helps!

2 Likes

Just a small addition:
I think the app will be a bit prettier if 0 is replaced with “Please select an answer” like this:

import streamlit as st
import numpy as np
import session_state
from streamlit.ScriptRunner import RerunException
from streamlit.ScriptRequestQueue import RerunData

state = session_state.get(question_number=0)

@st.cache
def get_question(question_number):
    arr = np.random.randint(0, 100, 2)
    q = f"{arr[0]} * {arr[1]}"
    ans = arr[0]*arr[1]
    choices = ["Please select an answer", ans, ans-1, ans+1, ans+2]
    return arr, q, ans, choices

arr, q, ans, choices = get_question(state.question_number)

st.text(f"Solve: {q}")
a = st.selectbox('Answer:', choices)

if a != "Please select an answer":
    st.write(f"You chose {a}")
    if (ans == a):
        st.write("Correct!")
    else:
        st.write(f"Wrong!, the correct answer is {ans}")
            
if st.button('Next question'):
    state.question_number += 1
    raise RerunException(RerunData(widget_state=None))

Another thought: You could also use the session state functionality to keep track of number of right vs wrong answers :slight_smile:

3 Likes

Thanks, Peter! This is very helpful. It opens up a lot of possibilities for other apps I have in mind where I would have to make use of loops and track state.