Discussion on dictionary based callback
Hi, A few months ago, I wrote a wrapper around callback to support dictionary handing of inputs and outputs in Dash. Unbeknownst to me you all ad a parallel effort. After many months of internal vetting, we determined it was ready for prime time and I submitted it as a PR https://github.com/plotly/dash/pull/1646.
I haven't had a chance to look at the dash-labs approach yet, but the method I propose is very simple to use and intuitive IMHO. It also supports pattern matching. (Took a while to see how to incorporate it). Some documentation is provided in the PR plus I've ported virtually every example in the Dash documentation using this callback. I'd be happy to see how we could potentially take the best of both and create a new better callback.
I've used this extensively inhouse so I'd be happy to show the various ways this can be used. In a way we use it to manage the problem of outputs being driven by many inputs for which the proscribed solution by dash is to create conglomerated callback.
Here's the documentation I prepared with the pull request.
The dictionary based callback decorator @app.dict_callback operates similarly to the normal @app.callback decorator. With two additional options strict and allow_missing. The invocation of the decorator is virtually identical.
Basic Example with State
This example is the same basic example given in the documentation
except using dict_callback.
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div([
dcc.Input(id='dcinp-1-state', type='text', value='Montréal'),
dcc.Input(id='dcinp-2-state', type='text', value='Canada'),
html.Button(id='dc-submit-button', n_clicks=0, children='Submit'),
html.Div(id='dc-output')
])
@app.dict_callback(Output('dc-output', 'children'),
Input('dc-submit-button', 'n_clicks'),
State('dcinp-1-state', 'value'),
State('dcinp-2-state', 'value'))
def update_output(inputs, states):
return {'dc-output.children': u'''
The Button has been pressed {} times,
Input 1 is "{}",
and Input 2 is "{}"
'''.format(inputs['dc-submit-button.n_clicks'],
states['dcinp-1-state.value'],
states['dcinp-2-state.value'])}
The strict and allow_missing options
For performance reasons, the dict_callback decorated callback doesn't by default check to make sure all keys in the output dictionary correspond to actual output ids and output properties declared in the invocation. The strict option enforces that. If the output contains a key not corresponding to a declared output id and property a KeyError is thrown. A good practice is to set strict to try while developing and debugging to catch possible errors. The allow_missing option is set to True by default. This options
allows for an incomplete output dictionary. All missing properties are unchanged. This is particularly useful for a complex callback where helper functions may be responsible for some outputs and not all outputs need to be updated.
In the following example, a KeyError is thrown since output-2-children is missing from the dictionary but if allow_missing were set to True it would be allowed. This pattern is very useful. Due to the constraints on outputs belonging to a single callback, you can easily be forced to conglomerate a bunch of callbacks into one. This pattern allows you to subdivide the callback into separate functions and one only needs to merge the dictionaries at the end.
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Input(id="input1", value="initial value"),
dcc.Input(id="input2", value="state"),
html.Div([html.Div(id="output-1"), html.Div(id="output-2")])
])
@app.dict_callback([Output("output-1", "children"), Output("output-2", "children")],
[Input("input1", "value")],
[State("input2", "value")], allow_missing=False)
def update_output(inputs, states):
output = {"output-1.children": inputs['input1.value']
}
return output
In this example, strict is set. This will throw a KeyError as there is no output called output-3.children. While this is usually a good thing, it requires a separate check so could impact performance.
app = dash.Dash(__name__)
app.layout = html.Div(
[
dcc.Input(id="input1", value="initial value"),
dcc.Input(id="input2", value="state"),
html.Div([html.Div(id="output-1"), html.Div(id="output-2")])
]
)
@app.dict_callback([Output("output-1", "children"), Output("output-2", "children")],
[Input("input1", "value")],
[State("input2", "value")], strict=True)
def update_output(inputs, states):
output = {"output-1.children": inputs['input1.value'],
"output-2.children": states['input2.value'],
"output-3.children": "Another Value",
}
return output
Finally, the dict_callback decorator works with pattern matching as in this Pattern Matching Example (taken from the documentation). Note that dict_callback works with a callback_dict which is a subclass of dict that provides pset and pget that allows access to the dictionary using pattern matching keys without having to worry about the underlying hashing or the order.
app = dash.Dash(__name__)
app.layout = html.Div([
html.Button("Add Filter", id="dynamic-add-filter", n_clicks=0),
html.Div(id='dynamic-dropdown-container', children=[]),
])
@app.dict_callback(
Output('dynamic-dropdown-container', 'children'),
Input('dynamic-add-filter', 'n_clicks'),
State('dynamic-dropdown-container', 'children'))
def display_dropdowns(inputs, states):
new_element = html.Div([
dcc.Dropdown(
id={
'type': 'dynamic-dropdown',
'index': inputs['dynamic-add-filter.n_clicks']
},
options=[{'label': i, 'value': i} for i in ['NYC', 'MTL', 'LA', 'TOKYO']]
),
html.Div(
id={
'type': 'dynamic-output',
'index': inputs['dynamic-add-filter.n_clicks']
}
)
])
states['dynamic-dropdown-container.children'].append(new_element)
return states
@app.dict_callback(
Output({'type': 'dynamic-output', 'index': MATCH}, 'children'),
Input({'type': 'dynamic-dropdown', 'index': MATCH}, 'value'),allow_missing=False
)
def display_output(inputs, states):
id_, _ = inputs.pkeys()[0]
output=dash.Dash.callback_dict()
div = html.Div('Dropdown {} = {}'.format(id_['index'], inputs.pget(id_,'value')))
output.pset(type='dynamic-output', index=id_['index'], property='children', value=div)
return output
Hi @summerswallow-whi, thanks for sharing your work here. Yeah, there is a bit of overlap here.
Since you've thought about this a good bit already, it would be helpful to hear your feedback on https://github.com/plotly/dash-labs/blob/main/docs/02-CallbackEnhancements.md if you're interested in looking it over.
I have taken a look at the callback enhancement though not in great code detail and compared our approaches. I think fundamentally there is a difference in philosophy. You mentioned in the code that for now it is a wrapper about the old callback. So to illustrate the difference if we were to pseudo-code your callback enhancement with decorators it might look something like this
@enhancement (...)
@app.callback(...)
def callback {
}
If I were to refactor my code as a decorator (which is actually how I did start out), it would look more like this
@app.callback(...)
@dictonary(...)
def callback() {
}
Despite addressing the same problem, the solutions actually don't overlap. You wrap the encompassing callback mechanism including the callback decorator whereas my approach encompasses the underlying callback.
Both approaches have their advantages and disadvantages. The chief advantage of your existing approach is that you can mix and match dictionary vs tuples and there are cases where that can be a significant advantage. If someone has an array of inputs a tuple makes iteration over them much easier. My approach commits to using a dictionary, plus I have found with my approach in simple cases forcing the output into a dictionary is a little cumbersome.
I think the two chief advantages to my approach is first it doesn't build up another framework for the callback mechanism. You already have tuples, and recently pattern matching. I would be a little concerned that adding dictionaries on top of this would make maintaining compatibility and maintenance down the road more complicated. The second advantage is that the coder doesn't have to conceive and construct a dictionary as input or output. It comes for free as an id.property' key.
All this being said, even though the two approaches are philosophically different, they are not incompatible and could coexists. As such there may even be an opportunity for some a-b testing. In fact one alternative would be for me to refactor the code back to a decorator.
I haven't looked at the code in detail but mostly at your documentation. Two things aren't clear to me. First how does this work with pattern matching? Looks like maybe simple cases should just work, but I'm not as clear on how that would work on more complicated cases. (I originally wrote this code probably 6 or 7 months ago and pattern matching threw me for a loop) It took a while to figure out a way to make my code compatible. If you have issues here, maybe somethings I wrote might address this and would be happy to apply what I've learned. The second thing (and I apologize if it is apparent from the code) how does the dictionary structure reflect in the callback_context input_lists and output_lists.
As for direct feedback on that document, I think showing some pattern-matching examples with keywords would be good. Also, I think the interchanging of inputs and states is a great idea. I've found that when I use my callback code, the first two lines of the callback are
def callback(inputs, states):
inputs.update(states)
So putting they into the same bucket makes a lot of sense to me.
I took a broader look at your documentation, I think your templates is a fantastic idea. I had some thoughts but didn't know whether to create a separate issue. One of Dash's shortcomings in building complicated complicated dashboards and interactions has been the ability to abstract sets of components. Looks like templates can fill that shortcoming.
Thanks for the rundown. And yeah, I do need to do some more testing with pattern matching callbacks, so that's a good reminder.
When actually integrating this into Dash, it may not keep the same design of being a wrapper around the callback mechanism, that that's still TBD. On idea, have you thought about implementing your dict_callback as a Dash plugin? The plugin API isn't really documented, but it's pretty simple, and you could probably pretty much copy what I did with the FlexibleCallback callback plugin here. This would make it possible to distribute what you have as separate package that folks could try out and discuss more on the forums.
And actually, an advantage of my current approach of wrapping the Dash 1 callback decordator is that your Plugin could do the same thing, without needing to unify the plugin functionality right away.
Thanks for taking a look at the template. The goal is certainly to address some of the shortcomings you mentioned. The best place to discuss templates right now would be this forum thread: https://community.plotly.com/t/dash-labs-0-2-0/52952/12. Looking forward to hearing your thoughts!
I'll give the plugin a go. Is there a forum thread for this topic?
Yes, as Jon said, the thread is here: https://community.plotly.com/t/dash-labs-0-2-0/52952/12
Please note that the dash-labs plugin-based system is an interim state, and the end-goal here is that the "real" app.callback will be modified for the 2.0 release to adopt the behaviour currently being demo'ed by the plugin, so in the end there will be no decorator to implement the changes that dash-labs exposes.
Maybe you'll want to implement your proposed behaviour as a separate plugin for demonstration/distribution/adoption as well?
Had to jump through a few approvals at work but I open sourced the dict_callback as a plugin.
It's now available at Pypi pip install dash-dict-callback if anyone wants to kick the tires on it. More examples are forthcoming in the repo. You can find the source at https://github.com/WestHealth/dash-dict-callback.