Error - Expected the output type to be a list or tuple but got None

I have a callback that is triggered by pressing any of the two buttons - ‘orders_button’/‘dashboard_button’.

@app.callback(
                [Output(component_id='url', component_property='pathname'),
                 Output(component_id='page_header', component_property='children')],
                [Input(component_id='orders_button', component_property='n_clicks'),
                Input(component_id='dashboard_button', component_property='n_clicks')]
            )
def process_home(n_clicks_orders, n_clicks_dashboard): 
    ctx = dash.callback_context
        
    if (ctx.triggered and ctx.triggered[0]['value'] != 0):
        trigger_input_id = ctx.triggered[0]['prop_id'].split('.')[0]
    
        if trigger_input_id == 'orders_button':
            return ('/orders', page_header_1)
        elif trigger_input_id == 'dashboard_button':
            return ('/dashboard',page_header_2)

When I run the app, I see a warning message (the program still runs):

dash.exceptions.InvalidCallbackReturnValue: The callback …url.pathname…page_header.children… is a multi-output. Expected the output type to be a list or tuple but got None.

What puzzles me is that this message appears in my console even before the callback is triggered i.e. even before I have pressed any of the above two buttons. I have confirmed that the callback is not triggered when this message appears by debugging.

Am I doing something silly or is it a bug ?

Interestingly, if I add the following two lines to the end of the callback, the warning disappears:
else:
return (no_update, no_update)

Adding the above two lines seems to replace the None with no_update.

I don’t understand this. What is going on ?

1 Like

Dash fires all callbacks when the app loads, thus your original code did not have a valid return, hence needing the else clause you subsequently added.

1 Like

@flyingcujo thank you so much. I didn’t realize that.

Based on that knowledge, I changed the callback last line to display a default layout before any button is pressed:

@app.callback(
                [Output(component_id='url', component_property='pathname'),
                 Output(component_id='page_header', component_property='children')],
                [Input(component_id='orders_button', component_property='n_clicks'),
                Input(component_id='dashboard_button', component_property='n_clicks')]
            )
def process_home(n_clicks_orders, n_clicks_dashboard): 
    ctx = dash.callback_context
        
    if (ctx.triggered and ctx.triggered[0]['value'] != 0):
        trigger_input_id = ctx.triggered[0]['prop_id'].split('.')[0]
    
        if trigger_input_id == 'orders_button':
            return ('/orders', page_header_1)
        elif trigger_input_id == 'dashboard_button':
            return ('/dashboard',page_header_2)
        else: # default
            return ('/orders', page_header_1)

However, now I find that the default layout appears but page is constantly refreshing. It refreshes about 20 times or so continuously and then the default layout disappears from the page. However, in the Python console I see that the page is still refreshing even though the default layout has disappeared.

What is going on there ?

Not sure what’s going on w/o the rest of your code. Have you identified which callback is being triggered?

Also, what happens when if (ctx.triggered and ctx.triggered[0]['value'] != 0) is not satisfied? There is no return value. I would move your else clause to be associated with this vice if trigger_input_id.

I have worked out that the last problem was occurring because I was trying to work around a bug in Dash (https://github.com/plotly/dash/issues/1049#issuecomment-589754586).

I have now resolved this issue at least. Thanks for your help.

1 Like

The reason is that you don’t have an “else” for your top-level “if”. You would need:

def process_home(n_clicks_orders, n_clicks_dashboard): 
    ctx = dash.callback_context
        
    if (ctx.triggered and ctx.triggered[0]['value'] != 0):
        trigger_input_id = ctx.triggered[0]['prop_id'].split('.')[0]
    
        if trigger_input_id == 'orders_button':
            return ('/orders', page_header_1)
        elif trigger_input_id == 'dashboard_button':
            return ('/dashboard',page_header_2)
        else: # default
            return ('/orders', page_header_1)
    else:
        return '', ''
2 Likes

Thanks @zoeyrose. I coded this with:

return (no_update, no_update)

after importing:

from dash import no_update

2 Likes

That drove me insane! Thank you, solved my problem!

1 Like