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":
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
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.
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 asDash()receives any other name — a literal string, orDash(__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:Environment
dash[fastapi])Minimal reproduction
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":Root Vause
dash/_utils.py:alias_main_moduleonly aliases the running module intosys.moduleswhencaller_nameis exactly"__main__"/"__mp_main__". Passing an explicitname(or building the app anywhere other than the entry module's top level) makescaller_namesomething 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
Works temporarily but idk if that will break in any future releases.