Proposal: New callback syntax closer to function signature

Based on this feature request, I have been playing around with a new callback syntax that resembles the function signature more closely. To illustrate the idea, consider this (very simple) app,

from dash import callback, html, dcc, Input, Output, State, Dash

@callback([Output("hello", "children"), Output("click", "children")],
          Input("btn", "n_clicks"), State("name", "value"))
def hello(n_clicks: int, name):
    return f"Hello {name}!", f"Click count is {n_clicks}"

app = Dash(prevent_initial_callbacks=True)
app.layout = html.Div([
    dcc.Input(placeholder="Enter your name here", id="name"),
    html.Button("Click me!", id="btn"),
    html.Div(id="hello"), html.Div(id="click")
])

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

that prints hello and click count messages on click. With the new syntax, the code would be,

from dash_extensions.enrich import DashProxy, callback, html, dcc, prop

@callback
def hello(n_clicks: prop("btn", "n_clicks", int), 
          name: prop("name", "value", trigger=False)) \
        -> tuple[prop("hello", "children"), prop("click", "children")]:
    return f"Hello {name}!", f"Click count is {n_clicks}"

app = DashProxy(prevent_initial_callbacks=True)
app.layout = html.Div([
    dcc.Input(placeholder="Enter your name here", id="name"),
    html.Button("Click me!", id="btn"),
    html.Div(id="hello"), html.Div(id="click")
])

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

A few notes on the new syntax,

  • The Input / Output / State objects are replaced by a common prop function that takes component id and component property as mandatory arguments, and optionally a type definition
  • Argument props are interpreted as Input, or State if trigger=False
  • Return props are interpreted as Output, or ServersideOutput if serverside=True

On thing I like about the new syntax is that the binding to the prop happens just next to the argument name definition, e.g.

n_clicks: prop("btn", "n_clicks", int)

That means that you don’t have to look elsewhere in the code (with the current syntax, in a list above that could be arbitrarily long) to figure out what property (i.e. “n_clicks” of “btn”) the “n_clicks” argument binds to. I think that improves code readability.

Secondly, the new syntax is more compact (at least in the current form). Rather than importing “Input/Output/State”, you only need a single “prop” import. And the dependency type (i.e. “Input”, “Ouput”, etc.) is derived from the function signature. Especially for people that know Python, I think this might feel more “natural” as compared to learning a syntax just for Dash (i.e. the current “Input/Output/State” syntax).

You should be able to try out the new syntax with the following rc release,

Let me know what you think. Any suggestions for improvements are very welcome :slight_smile:

4 Likes

:heart_eyes: This is really, really cool. I like the syntax for the same reasons as you mention: terser & clearer relationship between prop and input variable.

I’d love to hear from community members on whether they would find this syntax more or less intimidating than the existing syntax.

Some thoughts and observations on learnability:

  • The words Input and Output were designed to have clear intention
  • In both cases, there is some uncommon python stuff: big callbacks decorators in the former and type signatures in the latter
  • I could see trigger being more obvious than “State”

Also, which versions of Python support this kind of typing?

I completely agree about the “unusually big callback decorators” being the “odd one out”. It is indeed not that common in Python. At the top of my head, the only way I can think of to reduce the size though would be to replace the “prop” function by something shorter, e.g. “P” (abbreviation for property). That might make the code less readable though, and the user already has the import to rename the import anyway if one likes to, i.e.

from dash_extensions.enrich import DashProxy, callback, html, dcc, prop as P  # or we could just rename prop to P

@callback
def hello(n_clicks: P("btn", "n_clicks", int),  name: P("name", "value", trigger=False)) \
        -> tuple[P("hello", "children"), P("click", "children")]:
    return f"Hello {name}!", f"Click count is {n_clicks}"

This is kind of similar to the common pattern of importing functions as F in pyspark.

I understand why the Input and Output names were chosen, but I actually remember being confused when I started working with Dash due to the presence of both a dcc.Input component (a common layout component) and the Input dependency component. That would be solved by the new syntax.

The current implementation relies on features introduced in Python 3.9, but it might be possible to provide an implementation for earlier versions of Python also. I am not sure.

EDIT: I believe that an implementation via PEP 3107 should be possible, in which case the minimum requirement would be Python 3.0, i.e. basically any (modern) Python version

3 Likes

Wow, great progress in helping newcomers to Dash! I remember being a little daunted by the way we had to handle callbacks. I do have a few points to add to the discussion:

  1. Easier to learn? I think this could be easier because you don’t have to learn how to mesh together the idea of inputs outputs, and state. I think combining the id with the variable name is nice, but possibly something that would make this more ‘readable’ is a an argument prop that is more descriptive? Instead of ‘trigger=false’ for Input or State, why not have a ‘trigger_type’ or something and make it equal to ‘input, output, or state?’ Readability and ease of understanding is super important I believe.
  2. Advantages/Disadvantages: I think the advantages are pointed in terms of understanding the id and variable plus the type. Disadvantage is that I think that it is a little harder to understand what is going on. This is biased from my time with the current syntax, but looking through and seeing Output() and Input() I know what is an input and an output. I honestly do not mind importing in Input, Output, State vs. just importing over prop. That doesn’t bother me because it is just a few extra words at the top of the code which add to the readability of the code.
  3. Would I personally convert over to this style of writing a callback: I do like the optional type definition and how it combines the id and the variable. I wonder if we can do that but at the same time make it as readable and understandable? Maybe instead of a generic prop you just have new Input, Output, State (so as not be misunderstood with the current ones) that do the same thing (replace the generic prop)? I think by doing so you aid in the readability (put the id and the variable name together) plus retain the original idea of having separate input, output, state identifiers.

In summary, I think that the new callback syntax is a step in the right direction, but there might be some improvements that could be done to enhance readability (prop is not as easily understood as Input or Output).

3 Likes

Long winded feedback from a semi newbie to Dash:

1d

  1. Would this be easier to learn for Python beginners compared to the traditional callback architecture? Why?
  • The words Input, Output, State make it more clear than a generalized prop property as to which part you are assigning. Whichever way you go, it should be clear and easy to understand if the order is important.
  • I think the → is the function annotator (?) that was new in Python 3, and I’m not that familiar with using it or reading its role in this proposed code structure. So for Python beginners picking that up quickly might depend on their background and what focus they’re coming from in learning Python whether they would have used this before or not (maybe less so in a data viz/analysis standpoint v. data engineering work?).
  1. What are some advantages and drawbacks of this new architecture?
  • in the current structure I know I need to determine and state explicitly whether each part is output, input, or state. It is required you might say.
  • in the proposed structure, the argument props are interpreted as Input , or State if trigger=False . In this sense trigger is an optional property, and it could be easy to misunderstand when it’s needed or how it is interpreted unless you a really good at reading the docs while you code early on (which we all should be of course, but…). Might force people to break the flow to go out and check docs if they don’t code callbacks often.

In other words, argument properties are optional but it seems like you’ll get errors or odd results if you forget one or don’t fully understand when it is needed (as the coder you have not explicitly named your intentions, so would the error messages be as helpful to direct you toward your goal?).

  • An advantage for someone with more functions experience with the proposed structure would be familiarity I suppose. I’ve written more functions in R than Python, whereas for some Python users this new syntax would be more familiar than the current callbacks. For me, learning the current callback structure was like learning a specific standalone thing.
  1. As a more advanced Dash user, would you prefer using this new way of writing out a callback? Why?
  • Since I’m still actually pretty new to using Dash, I could learn either of course. Seems like now the only purpose of writing @callback is to introduce a callback. Whereas before it held more weight with more components.
  • Having a link in web docs about proposed structure to helpful background on function would be helpful to understand all the parts which are not specific to Dash.
2 Likes

I really like this proposal and think it would be a nice addition to Dash. Perhaps it could be made available as one of the “Flexible Callback Signatures”

As mentioned previously, the current way of writing callbacks is unique to Dash. Once you “get it”, I think it’s harder to see why a different way might be better. But the learning curve can be steep for people new to Dash - as this infamous post summarized :slight_smile:

I think there are advantages to having the Dash callback structure similar to how it’s done with annotations, because the annotations are part of the standard Python library - even if it’s not well known.

I also agree that this proposed way is great for when there are lots of inputs and output.

It would also make it easier in the case when an input just triggers the callback and is not used in the callback function - like is common with dcc.Interval. It would be clear that you still need a variable name, even if it’s not used in the callback function.

2 Likes

For most use cases I feel that the proposed style is superior and should probably be adopted as the standard way of declaring dash callbacks.

It does get quite annoying having partial duplication of variable definition. It is often hard for newcomers to form a link in their mind between the callback parameters and the function parameters.

3 Likes

Hi folks,

Thanks for this nice discussion!

In terms of new users, I have the impression (from the questions I try to help) that this would be a slightly easier way to write the callbacks, however I would not put the decorators or matching function parameters to decorator parameters in the top of things that cause problems for beginners with respect to callbacks. I am pretty sure it is confusing, but as said by others, once you learn it, you learn it.

I will play devil’s advocate for a bit, please take with a grain of salt because this is more a matter of personal preference and a piece of opinion than anything else.

First, I think the example provided in dash-extensions that triggered the implementation by @Emil is a bit particular, but I certainly understand how it can be extremely hard to debug it. It would have been better if the same example leveraged the keyword-based signature available now (instead of just follow position). It would not have solved the issue of having two definitions in very distant lines, but at least one would have to match the dictionary keys in the decorator with the parameter name in the function argument.

Second, I have a certain bias. When I see function annotation, I think of type hinting and things that don’t matter at runtime, but not added functionality to the function. I know that the semantics of function annotation is not well defined on purpose, on the other hand I have the impression that type hinting is getting very popular and might “take over” this functionality.
Besides, still on this point, I can think of many examples where decorators are used to add context to a particular function (to name a few, Flask, pytest, Click, Tensorflow), but I never saw any good example of annotations being used for the same purpose. Again, it can my personal bias or just that function annotations are a new feature, I don’t know… I prefer to have an obvious way to use the language, and to me the obvious way to add context to a function is wrapping it in decorators and not using annotations.

In summary, I believe that the issues mentioned regarding the current syntax are not “big enough” to justify changing it, but I am ready to be convinced otherwise. :slight_smile:

4 Likes