St.file_uploader keeps calling the callback function passed into the 'on_change' parameter on every other widget change

Streamlit v1.3.0

I only want the file_uploader to run the callback function when I remove any file or upload any new file to the file_uploader widget. But now I’m having this issue where it keeps calling the ‘on_change’ callback function even though I didn’t remove or upload any file. The way to trigger this is just to change any other widget’s value (e.g. the st.radio button in the example code below).

But I know I’m actually changing it’s value when I tried to access it to copy the file to a temporary folder to use it to do something. So I tried to use video_file.seek(0) and also deepcopy(video_file) to try to avoid changing the value of the file_uploader, but still in vain. Please help.

import streamlit as st
import shutil
import os
from copy import deepcopy
from pathlib import Path

TEMP_DIR = Path('tmp/extracted')


def dummy_cb():
    print("DUMMY CALLBACK FOR FILE UPLOADER")


video_file = st.sidebar.file_uploader(
    "Upload a video", type=['mp4', 'mov', 'avi', 'asf', 'm4v'],
    key='video_file_uploader', on_change=dummy_cb)

if video_file is None:
    st.stop()

# use this to avoid keep calling the file_uploader's callback
# NOTE: still not helping...
uploaded_video = deepcopy(video_file)

video_path = str(TEMP_DIR / uploaded_video.name)

print("Copying file")
if TEMP_DIR.exists():
    shutil.rmtree(TEMP_DIR)
os.makedirs(TEMP_DIR)
print(f"{video_path = }")
with open(video_path, 'wb') as f:
    f.write(uploaded_video.getvalue())

# seeking is also not helping
uploaded_video.seek(0)
video_file.seek(0)

st.radio("options", (1, 2), key='dummy_options')
st.stop()
1 Like

I settled with a workaround by performing all the necessary callbacks within the if video_file is None statement. This works well for accepting a single upload but probably not multiple files.

if video_file is None:
    logger.info("Resetting states for uploaded video")
    # reset and remove the TEMP_DIR if there's no uploaded file
    # to ensure the app only reads the new uploaded file
    reset_cb()
    if TEMP_DIR.exists():
        shutil.rmtree(TEMP_DIR)
    os.makedirs(TEMP_DIR)
    st.stop()

# using str to ensure readable by st.video and cv2
video_path = str(TEMP_DIR / video_file.name)

if not os.path.exists(video_path):
    # only creates it if not exists, to speed up the process when
    # there is any widget changes besides the st.file_uploader
    logger.debug(f"{video_path = }")
    with st.spinner("Copying video to a temporary directory ..."):
        with open(video_path, 'wb') as f:
            f.write(video_file.getvalue())
1 Like

Hi, I am having the same issue. I’ve created a demo app to replicate the issue so that it can hopefully be addressed sooner.

import streamlit as st

def uploader_callback():
    print('Uploaded file')

st.file_uploader(
    label='File uploader',
    on_change=uploader_callback,
    key='file_uploader'
)

st.text_input(label='Textbox 1', key='first')

Bug: If another widget gets updated, e.g. st.text_input, the callback function passed into ‘on_change’ of st.file_uploader gets reran even if the file has remained the same.

Desired: st.file_uploader on_change to behave as it should - only running when the value of st.file_uploader is changed

1 Like
Streamlit, version 1.7.0

I also tried the on_change callback of the st.file_uploader widget and found that the callback function is called twice for each upload, except for the very first time.
Is this intentional?

3 Likes

Same problem on Streamlit version 1.8.1

facing the same issue here, anyone solved it yet?

Hi all :wave:

I was able to reproduce this bug with Streamlit v1.10.0 and have submitted a bug report:

1 Like

Thank you, would you recommend any workaround for the mean time as I have an ML App that is currently running.

@nick_muchi My understanding of this behavior is that each subsequent upload also clears the existing upload. Clearing the existing upload triggers the callback, too. For each new upload, the callback is called when the existing upload is cleared AND when the new file is uploaded.

A workaround is to execute the callback if the contents of the uploaded file is not None. Doing so ensures the callback contents are not executed when the existing upload is automatically cleared during a new upload. Here’s an example of the workaround:

import streamlit as st

if 'ctr' not in st.session_state:
    st.session_state['ctr'] = 0

def uploader_callback():
    if st.session_state['file_uploader'] is not None:
        st.session_state['ctr'] += 1
        print('Uploaded file #%d' % st.session_state['ctr'])

st.file_uploader(
    label="File uploader", on_change=uploader_callback, key="file_uploader"
)

fileuploader-bug-workaround

Best, :balloon:
Snehan

1 Like

Thanks for your help, I think my issue is a bit different but related to the file_uploader callback, hope you can assist on that if you have time. I tried but could not come up with a workaround.