plotly / plotly/dash

[BUG] Callbacks still double-register with uvicorn `--reload` when an explicit `name` is passed to `Dash()` (incomplete fix for #3818 / #3883)

Open
#3,912 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement P3 size: 10+
Dominant language
Python
Stars
24.4k
Forks
2.3k
Avg merge
2d 7h
Merged PRs (30d)
13

Description

Summary

Thanks for the fix! This is a follow up issue from #3818 . PR #3883 (released in 4.4.1) fixes the double-registration reported in #3818 but only for the exact idiom app = Dash(__name__) written at the top level of the entry-point file.

The fix (alias_main_module) keys entirely on the app's resolved name being "__main__" or "__mp_main__". As soon as Dash() receives any other name — a literal string, or Dash(__name__) when the app is built inside a package / app-factory (where __name__ is the module name) — the alias no-ops, the module is executed twice in the reload worker, and every callback is registered twice. The renderer then throws:

Duplicate callback outputs
Output 0 (output.children) is already in use.

Environment

  • dash 4.4.1 (dash[fastapi])
  • uvicorn (standard), reload=True
  • Python 3.10, Linux

Minimal reproduction

# repro.py
from dash import Dash, html, dcc, callback, Output, Input

# BUG TRIGGER: any explicit name that is not "__main__"/"__mp_main__".
# Using Dash(__name__) here (top level) works; Dash("myapp") does not.
app = Dash("myapp", backend="fastapi")
app.layout = html.Div([dcc.Input(id="input", value="Hello"), html.Div(id="output")])

@callback(Output("output", "children"), Input("input", "value"))
def update(value):
    return f"You typed: {value}"

app.enable_dev_tools(debug=True, dev_tools_hot_reload=True)
server = app.server

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("repro:server", host="127.0.0.1", port=8050, reload=True)
python3 repro.py
curl -s http://127.0.0.1:8050/_dash-dependencies | grep -o '"output.children"' | wc -l
# -> 2   (expected 1)

Open the page in a browser and the console shows Duplicate callback outputs.


Constructing the app inside a package with Dash(__name__) also reproduces it, because __name__ is the package name, not __main__, in this case -- __name__ == "app_factory":

  • app_factory.py:
from dash import Dash, html, dcc, callback, Output, Input
class AppBuilder:
    def __init__(self):
        self.app = Dash(__name__, backend="fastapi")
        self.app.layout = html.Div([
            html.H1("Reload double-register demo (factory)"),
            dcc.Input(id="input", value="Hello"),
            html.Div(id="output"),
        ])

        @callback(Output("output", "children"), Input("input", "value"))
        def update(value):
            return f"You typed: {value}"

    def setup(self):
        self.app.enable_dev_tools(debug=True, dev_tools_hot_reload=True)
        return self.app.server
  • main.py:
if __name__ != "__main__":
    from app_factory import AppBuilder
    server = AppBuilder().setup()

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:server", host="127.0.0.1", port=8050, reload=True)

Root Vause

dash/_utils.py:

def alias_main_module(caller_name: str) -> None:
    if caller_name not in ("__main__", "__mp_main__"):
        return
    ...

alias_main_module only aliases the running module into sys.modules when caller_name is exactly "__main__"/"__mp_main__". Passing an explicit name (or building the app anywhere other than the entry module's top level) makes caller_name something else, so the alias is skipped, the "main:server" import string re-executes the file, and the callbacks register a second time.

This means the fix effectively only covers the single-file app = Dash(__name__) tutorial layout, and misses the very common app-factory / class-based / explicitly-named app patterns.

Current Workaround

# Skip the reload worker's __mp_main__ spawn re-exec so callbacks are registered exactly once.
if __name__ not in ("__main__", "__mp_main__"):
    server = build_app()

Works temporarily but idk if that will break in any future releases.

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 in dash/_utils.py at alias_main_module and reproduce the issue with the provided explicit-name app and uvicorn reload configuration. Trace how the reload worker imports the server and registers callbacks; done means the dependency output contains each callback once for explicit names and app-factory patterns, without breaking the existing main behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
fastapi, python
Domain
backend, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.