plotly / plotly/dash

Support Dash prop callback connection closer to function signature

Open
#1,810 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature P3
Dominant language
Python
Stars
24.4k
Forks
2.3k
Avg merge
2d 7h
Merged PRs (30d)
13

Description

Is your feature request related to a problem? Please describe.
When creating complex Dash dashbords/callbacks, there can be a long list of input/state/output. We in some cases have seen things of the size of:

@app.callback(
    Output("someid", "someprop"),
    Output("someid", "someprop"),
    Output("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    Input("someid", "someprop"),
    State("someid", "someprop"),
    State("someid", "someprop"),
    State("someid", "someprop"),
    State("someid", "someprop"),
    State("someid", "someprop"),
    State("someid", "someprop")
)
def _some_call_back(
    arg1,
    arg2,
    arg3,    
    arg4,    
    arg5,    
    arg6,    
    arg7,    
    arg8,    
    arg9,    
    arg10,    
    arg11,
    arg12,    
    arg13,    
    arg14,    
    arg15,    
    arg16,    
):
    ...

😱 Both during development, but also code review/maintenance, there is mental overhead with respect to seeing which (id, prop) pair belongs to which named argument in the function. This has gotten a bit better with https://dash.plotly.com/flexible-callback-signatures, but the underlying problem with (id, prop) and named argument in the function being defined in two different lines remains.

Describe the solution you'd like
Utilize the Python 3+ typing syntax to annotate the (id, prop) closer to the argument. By using that we at the same time can avoid explicitly stating Output as that is already explicit by being defined as the returned type(s) of the function. The only thing to differentiate between is Input/State which could be done by some extra annotation tag. E.g. something like

from typing import Annotated as A

@app.callback
def _some_call_back(
    arg1: A[List[str], "someid", "someprop"],
    arg2: A[List[str], "someid", "someprop"],
    arg3: A[List[str], "someid", "someprop"],
    arg4: A[List[str], "someid", "someprop"],
    arg5: A[List[str], "someid", "someprop", "state"],
) -> Tuple[
        A[List[str], "someid", "someprop"],
        A[List[str], "someid", "someprop"],
        A[List[str], "someid", "someprop"],
    ]:
    ...

Another benefit is that we encourage Dash app developers to type hint their callback functions which again helps IDEs and mypy detect bugs (related to #1786).

Additional context

See https://docs.python.org/3/library/typing.html#typing.Annotated for documentation on typing.Annotated (backported to earlier Python 3 versions in typing_extensions).

Related to #1748 and #1786, but implementation is independent of those.

PoC code showing how type annotations potentially can be transformed into the Input/Output/State when callbacks are registered/decorated:

The below code will print out

Output('id1', 'propname')
Output('id2', 'propname')
Input('id3', 'propname')
Input('id4', 'propname')
State('id5', 'propname')
State('id6', 'propname')
State('id7', 'propname')
#############
# Dash core #
#############
import inspect

# In the std.lib typing library on Python 3.8+:
from typing_inspect import get_args, get_origin


def callback(func):
    for name, annotation in inspect.getfullargspec(func).annotations.items():
        if name == "return":
            if get_origin(annotation) == tuple:
                # Multi valued output
                for return_value in get_args(annotation):
                    print(f"Output{return_value.__metadata__}")
            else:
                print(f"Output{annotation.__metadata__}")
        else:
            if len(annotation.__metadata__) >= 3 and annotation.__metadata__[2] == "state":
                print(f"State{annotation.__metadata__[:2]}")
            else:
                print(f"Input{annotation.__metadata__}")

    return func


######################
# Dash app developer #
######################

from typing import List, Tuple
from typing_extensions import Annotated as A  # On Python 3.9+ this is in typing std.lib


@callback
def _update_plot(
    arg3: A[List[str], "id3", "propname"],
    arg4: A[List[str], "id4", "propname"],
    arg5: A[List[str], "id5", "propname"],
    arg6: A[List[str], "id6", "propname", "state"],
    arg7: A[List[str], "id7", "propname", "state"]
) -> Tuple[
       A[List[str], "id1", "propname"],
       A[List[str], "id2", "propname"] 
    ]:
    ...

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the @app.callback decorator and read the Annotated typing documentation and the PoC in this issue. Trace how callback inputs, states, and outputs are currently registered, then define the supported annotation behavior and verify that annotated callbacks produce the corresponding dependencies while preserving type information.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.