Skip to content

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

Description

@essol-li

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions