šŸ“£ Dash v1.11.0 Release - Introducing Pattern-Matching Callbacks

I am trying to use pattern-matching callbacks but it does not work.
Here is a minimal non-working example (modified example from the documentations)

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH, ALL

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets,
                suppress_callback_exceptions=True)

app.layout = html.Div([
    html.Button("Add Filter", id="add-filter", n_clicks=0),
    html.Div(id='dropdown-container', children=[]),
    html.Button("Rmove Filter", id="remove-filter", n_clicks=0),

])

@app.callback(
    Output('dropdown-container', 'children'),
    [Input('add-filter', 'n_clicks')],
    [State('dropdown-container', 'children')])
def display_dropdowns(n_clicks, children):
    new_dropdown = dcc.Dropdown(
        id={
            'type': 'filter-dropdown',
            'index': n_clicks
        },
        options=[{'label': i, 'value': i} for i in ['NYC', 'MTL', 'LA', 'TOKYO']]
    )
    children.append(new_dropdown)
    return children

# BEGIN -- trouble making callback
@app.callback(
    Output('dropdown-container', 'children'),
    [Input('remove-filter', 'n_clicks')],
    [State('dropdown-container', 'children')])

def display_dropdowns_2(n_clicks, children):
    children = children[0:-1]
    return children
# END -- trouble making callback


if __name__ == '__main__':
    app.run_server(debug=False)

Any given output (Output('dropdown-container', 'children') in this case) can only be set by one callback. So what youā€™d need to do in this case is combine the two callbacks into one, with both buttons as inputs, looking at dash.callback_context.triggered to figure out which was pressed. Check out the edit_list callback in the Todo App at the end of the pattern-matching docs page for an example of this:

@app.callback(
    [
        Output("list-container", "children"),
        Output("new-item", "value")
    ],
    [
        Input("add", "n_clicks"),
        Input("new-item", "n_submit"),
        Input("clear-done", "n_clicks")
    ],
    [
        State("new-item", "value"),
        State({"index": ALL}, "children"),
        State({"index": ALL, "type": "done"}, "value")
    ]
)
def edit_list(add, add2, clear, new_item, items, items_done):
    triggered = [t["prop_id"] for t in dash.callback_context.triggered]
    adding = len([1 for i in triggered if i in ("add.n_clicks", "new-item.n_submit")])
    clearing = len([1 for i in triggered if i == "clear-done.n_clicks"])
    new_spec = [
        (text, done) for text, done in zip(items, items_done)
        if not (clearing and done)
    ]
    if adding:
        new_spec.append((new_item, []))
    new_list = [
        html.Div([
            dcc.Checklist(
                id={"index": i, "type": "done"},
                options=[{"label": "", "value": "done"}],
                value=done,
                style={"display": "inline"},
                labelStyle={"display": "inline"}
            ),
            html.Div(text, id={"index": i}, style=style_done if done else style_todo)
        ], style={"clear": "both"})
        for i, (text, done) in enumerate(new_spec)
    ]
    return [new_list, "" if adding else new_item]

1 Like

As noted by @alexcjohnson, an output can only be targeted once in Dash, and the solution is to group callbacks accordingly. Since this can sometime make the code less readable, i have written a small utility class that takes care of this part. Here is an example with your code,

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
from dash_extensions.callback import DashCallbackBlueprint

# Create app (no changes here)
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets,
                suppress_callback_exceptions=True)
app.layout = html.Div([
    html.Button("Add Filter", id="add-filter", n_clicks=0),
    html.Div(id='dropdown-container', children=[]),
    html.Button("Remove Filter", id="remove-filter", n_clicks=0),

])

# Create callback blueprint object. Callbacks must target this objects instead of the app object.
dcb = DashCallbackBlueprint()


@dcb.callback(Output('dropdown-container', 'children'),
              [Input('add-filter', 'n_clicks')],
              [State('dropdown-container', 'children')])
def display_dropdowns(n_clicks, children):
    new_dropdown = dcc.Dropdown(
        id={'type': 'filter-dropdown', 'index': n_clicks},
        options=[{'label': i, 'value': i} for i in ['NYC', 'MTL', 'LA', 'TOKYO']]
    )
    children.append(new_dropdown)
    return children


@dcb.callback(Output('dropdown-container', 'children'),
    [Input('remove-filter', 'n_clicks')],
    [State('dropdown-container', 'children')])
def display_dropdowns_2(n_clicks, children):
    children = children[0:-1]
    return children


# Register callbacks on the app object. This call merges callbacks if needed.
dcb.register(app)

if __name__ == '__main__':
    app.run_server(debug=False)
2 Likes

Just to remove any ambiguity, when you refer to ā€œinputsā€ do you mean it in the strictest sense (i.e dash.dependecies.Input or are you referring to all components (i.e Input, Output, and State)?

Reason I ask, is because Iā€™m getting this error message with a pattern matching Output

Good question. And in fact exploring the edge cases of behavior in v1.10 added a little more nuance. I mostly mean strictly Input, but let me give some more detail:

  • If none of the Input items are present, we will not fire the callback, regardless of whether some, all, or none of the Output and State items are present. So if some of the Output and State items are present and others are missing, thatā€™s totally fine as long as none of the Input items are present.
  • The only exception is if all of the Input items have multi-item wildcards (ALL/ALLSMALLER) AND at least one Output item is present
  • If any Input items are present (or the multi-item exception), we will fire the callback, and itā€™s an error if any of the Output or State items is missing.

Now when an Output of a callback we DONā€™T fire is an Input to another callback, does that other callback fire on initial load? My first reaction was ā€œyes of course it should!ā€ but actually in v1.10 it depends: it will only fire if it has another Input, that either is not itself an Output, or is an Output of a callback that fired.

There is a rationale in which this exact pattern makes sense: if we think of the un-fired callback as effectively having raised a PreventUpdate. Thatā€™s behavior Dash has always had, a callback will not get called initially if all of its inputs were prevented from updating. And you could certainly say that this callback was prevented from updating its outputs by a lack of inputs!

On the other hand if we think of the un-fired callback as effectively not even existing until its inputs exist, then its output isnā€™t an output at all at that point, so the other callback should always get its initial call.

Iā€™m not sure one of these answers is more right than the other, so Iā€™m inclined to go with the PreventUpdate model for stability with v1.10 and before. But Iā€™m open to discussion (as long as itā€™s quick - Iā€™m working on the fix now :smile:)

2 Likes

Canā€™t edit my comment after a certain time, so hereā€™s what Iā€™ve done instead based on what @alexcjohnson recommended if it can help anyone.

  • For the custom view depending on the user: always registering the callback and returning either the componentā€™s layout or an empty layout ("" for example) actually works. It would just be like
child1.py

def layout():
    ### insert some layout here


# Callbacks 
@app.callback(Output('foo', 'foo_prop'), [Input('bar', 'bar_prop')])
def my_cb(value):
   ###
   # some logic here
   return new_foo_prop_value

####
parent.py

from children import child1
from flask_login import current_user

@app.callback(
   Output('child-container', 'children'),
   [Input('hidden-div-trigger', 'children')])
def render_child(_):
   if current_user.role <= child1.allowed_role:
       return child1.layout()
   else:
       return ""
  • Iā€™ve used the pattern matching in an admin panel to render a user management form which has three possible ā€œactionsā€: add (add a new user), edit (edit userā€™s username, email etc but not password), update (update password). Each action would correspond to a different version of the form layout.

Thereā€™s always only one form existing, it can just take several layouts. So all my components have a fixed index (0 here). But using the pattern matching prevents bugs where one of the input doesnā€™t exist anymore. For example, the username input doesnā€™t exist for the ā€œupdate passwordā€ version of the layout.

Looks like this :

add_edit_user_form.py

def layout(action="add", **kwargs):
	"""
	Same layout function for Add User / Edit User / Update Password with a minor 
	change depending on the action input
	args:
		action: "add", "edit", "update" (str)
	"""
	action = action.lower()
	index = 0


	rows = list()

	if action in ["add", "edit"]:
		# Username + email form row
		rows.append(username_email_row(index))
		# Role + Accessible sites form row
		rows.append(role_sites_row(index, role, sites))
	if action in ["add", "update"]:
		# Create / Update password + Confirm form row
		rows.append(password_row(index))

	return dbc.Card([
		dbc.CardBody([
			html.H1("H1"),
			html.Br(),
			dbc.Form([
				dbc.Row(dbc.Col(rows)),
				dbc.Button(btn_label, id={"type": f"{action}-user-form-btn", "index": index}),
				dbc.Alert(
			            "",
			            id="add-edit-user-alert",
			            is_open=False,
			            duration=4000,
			            dismissable=True
		        	),
			])
		])
	])


# And the callback associated 
@app.callback(
	[
		Output("add-edit-user-alert", 'is_open'),
		Output("add-edit-user-alert", 'children'),
		Output("add-edit-user-alert", 'color'),
		Output("single-user-action-store", "data")
	],
	[
		Input({"type": "add-user-form-btn", "index": ALL }, 'n_clicks'),
		Input({"type": "edit-user-form-btn", "index": ALL }, 'n_clicks'),
		Input({"type": "update-user-form-btn", "index": ALL }, 'n_clicks')

	],
	[
		State({"type": "add-edit-username-input", "index": ALL}, "value"),
		State({"type": "add-edit-email-input", "index": ALL}, "value"),
		State({"type": "add-edit-role-input", "index": ALL}, "value"),
		State({"type": "add-edit-sites-input", "index": ALL}, "value"),
		State({"type": "add-edit-password-input", "index": ALL}, "value"),
		State({"type": "add-edit-conf-password-input", "index": ALL}, "value"),
	])
def add_edit_update_user(add_n_clicks, edit_n_clicks, update_n_clicks, username, email, role, sites, password):

        # some more logic 
        # the only annoying part is since we need to match on index ALL 
        # (otherwise we would need to use the pattern MATCH on every input, output, state. 
        # At least using MATCH generated a bug which makes sense as we can create several callbacks for the same output ).
        # Because of this, inputs of the functions are array-like objects. I just do this to get them back as I know I always have 1-length arrays
        
        [add_n_clicks] = add_n_clicks

       # more logic and return

Thank you @alexcjohnson. The To Do example was very helpful.
One more question, is there any limitation on handling of exception for missing id in callbacks?

I ran into a problem with missing input and output callbacks and the resulting exception was not ignored.
Here is an example, where ā€œavailable-periodsā€ id exists but the other idā€™s did not. If I comment out any of the callbacks the app will at least run. However in the current state, the app will only say ā€œError loading dependenciesā€.


@app.callback(
    Output('available-periods', 'children'),
    [Input({'type': 'period-name-textbox', 'index': ALL}, 'value')]
)
def update_available_periods(values):
    return []

@app.callback(
    [Output({'type': 'filter-period-dropdown', 'index': ALL}, 'options')],
    [Input('available-periods', 'children')],
)
def update_selection_periods(values):
    return []

@jaser I think youā€™ve uncovered a bug :tada: - looks like something breaks when an ALL pattern is the only output but it doesnā€™t match any components. Weā€™ll get that fixed in the next release!

1 Like

Thanks for confirming this @alexcjohnson. I also tried to create a dummy component and use it in the output along the ALL pattern output but that did not work either.
Looking forward to future releasesā€¦

@alexcjohnson,

I notice that ctx.inputs_list is a bit greedy with what it captures. For instance, I have a callback below in which I want to capture just the value prop in one list, and the relayoutData in the other, but instead, I get all components and have to filter out the ones that donā€™t apply.

The filtering stage only adds two extra lines/per component, but it is rather inefficient as it requires me to iterate through every pattern matched component. Would it be possible to have this already filtered out before being put into the inputs_list?

Do you see any value add in having it unfiltered to begin with?

@app.callback(
    Output('state', 'data'),
    [
        Input({'module': ALL, 'id': ALL}, 'value'),
        Input({'module': ALL, 'submodule': ALL, 'id': ALL}, 'value'),
        Input({'module': ALL, 'submodule': ALL, 'id': ALL}, 'relayoutData'),
    ],
    [State('state', 'data')],
)
def update_state(*args):
    
    module_values, submodule_values, graph_relayouts = ctx.inputs_list

    module_values = [i for i in module_values if 'value' in i]
    if module_values:
        for value in module_values:
            handle_value(value, prev)

    submodule_values = [i for i in submodule_values if 'value' in i]
    if submodule_values:
        for value in submodule_values:
            handle_value(value, prev)

    graph_relayouts = [i for i in graph_relayouts if 'value' in i]
    if graph_relayouts:
        for relayout in graph_relayouts:
            handle_relayout(relayout, prev)


1 Like

When I upgraded to dash 1.11, my dcc.Interval stopped working. I used to dynamically set max_intervals through a button and then the interval would fire every interval Iā€™ve setup in the layout. It worked fine until dash 1.10. Now it doesnā€™t anymore. Did anything change under the hood? I couldnā€™t find anyone mentioning the same issueā€¦

Thanks everyone here who helped find edge cases and regressions in Dash 1.11. Dash 1.12 was released this afternoon with fixes for these issues, in addition to a bunch of new features - check it out! šŸ“£ Dash v1.12.0 Release - Pattern-Matching Callbacks Fixes, Shape-drawing, new DataTable conditional formatting options, and more

@mbkupfer I didnā€™t get a chance to look at your callback_context.inputs_list issue yet but I have not forgotten about it.

@MM-Lehmann Lots of things changed under the hood :slight_smile: I encourage you to try again with Dash 1.12. If the problem persists start a new topic here - feel free to @ mention me and Iā€™ll take a look but weā€™ll need a reproducible code snippet in order to help.

3 Likes

Thanks @alexcjohnson. Really appreciate the transparent communication and quick update. Canā€™t wait to try out 1.12!

2 Likes

Got it nailed down. I had quite a few circular dependencies in my app which didnā€™t pose a problem until dash 1.10. Somehow they now seem to interfere with threaded intervals (running alongside a long running callback). Actually they are blocking the entire callback chain of the circular depsā€¦
I wish there was an easy way of getting intermediate feedback from a long running callback. Whatever the change was, it became even more difficult now.

edit: found a more robust way now, avoiding circular deps. So this is a non-issue, I supposeā€¦

1 Like

Seeing the same console errors on my app. Found this thread on the React forum:

Seems like React got more picky recently and lots of people are bumping into this.

I think the verdict was that the issue was always present but the warnings only became apparent now. Nobody really can say if there is an impact at all.

Hi Alex, I got the above warning using dash 1.13.0. Is it still an unsolved problem? Thanks!

Hi Dash community,
In case anyone is interested, I was very excited when I learned to use pattern-matching callbacks, so I wanted to share what I know with others.

I created a tutorial that explains dynamic callbacks. I hope it helps others. This is really a Dash game-changer. Thank you to the Dash Plotly team that developed this.

5 Likes

@Lion Iā€™m not sure which warning youā€™re referring to - anyway this thread is getting long and hard to follow, maybe you can start a new thread with more specifics of what problem youā€™re seeing?

Iā€™m having performance trouble with this as well. I have a map with thousands of markers and a callback that is triggered when a marker is clicked. It can take a minute just to send all the thousands of components to the callback, and then once the function within the callback finally starts executing to filter out and get just what I need it only takes a couple seconds. Iā€™ll be following the convo in case any updates to this may be on the way. :grinning: