Hacktoberfest 2026: as issues que os mantenedores marcaram para outubro, abertas e boas para iniciantes. Ver issues do Hacktoberfest

Dash 4.4.1: alias_main_module() causes double callback registration when app entrypoint is run as `python <subdir>/<script>.py` from a parent directory

Aberta
#4,011 0 comentários 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 2 dias

Ninguém assumiu esta issue ainda.

Avaliação

Dificuldade
4/5
Tempo estimado
3-5 dias
Facilidade para iniciantes
68/100
Tipo de issue
Bug
Clareza
Claramente especificada
Status de atividade
Ativa
Stack de tecnologia
python

Direção de pesquisa

Reproduce the duplicate registration with the app/app.py and pages/home.py layout, then inspect dash/_utils.py (alias_main_module), its call in dash/dash.py, and the loader in dash/_pages.py. Verify the change against the shown /_dash-dependencies request: one page callback should be registered when the entrypoint is run from its parent directory, without regressing the reload-import case.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

Summary

dash.dash.alias_main_module() (added in #3883, to fix uvicorn.run("app:server", reload=True) double-import, #3818) causes a different double-import when an app's entrypoint file has a directory-name-equals-basename shape (e.g. app/app.py) and is launched with python app/app.py from the parent directory — the layout used by every "run the script directly" tutorial and by many real apps' READMEs. The self-reimport causes Dash's pages loader to execute every page module a second time, double-registering every page-level @callback, which breaks the client with "Duplicate callback outputs" errors.

Confirmed present in 4.4.1 and unchanged in 4.5.0rc0. Not present in 4.4.0 (no alias_main_module in that wheel) or 4.3.0.

Minimal repro

mkdir -p repro/app/pages
cat > repro/app/app.py <<'EOF'
from dash import Dash, html, dcc, callback, Output, Input
app = Dash(__name__, use_pages=True, pages_folder="pages")
app.layout = html.Div([dcc.Location(id="url"), dash.page_container])
if __name__ == "__main__":
    print("callbacks registered:", len(dash._callback.GLOBAL_CALLBACK_LIST))
    app.run(debug=False)
EOF
cat > repro/app/pages/home.py <<'EOF'
import dash
from dash import html, callback, Output, Input, dcc
dash.register_page(__name__, path="/")
layout = html.Div([html.Button("go", id="btn"), html.Div(id="out")])

@callback(Output("out", "children"), Input("btn", "n_clicks"))
def show(n):
    return f"clicked {n}"
EOF
cd repro
python app/app.py

Then, in a second terminal:

curl -s http://127.0.0.1:8050/_dash-dependencies | python3 -m json.tool

Expected

One Output("out", "children") callback registered — a single entry in /_dash-dependencies for that output.

Actual

The Output("out", "children") callback (and every other page-level callback) is registered twice, byte-identical, because pages/home.py is exec_module'd twice by Dash's pages loader (dash/_pages.py, unconditional spec.loader.exec_module(), no sys.modules check). In a full app this manifests client-side as "Duplicate callback outputs".

Root cause

alias_main_module() (dash/_utils.py, called from dash/dash.py:511, first statement in Dash.__init__) does, roughly:

import_name = canonical_import_name(module_file)   # relpath(app/app.py, cwd) -> "app.app"
if import_name is None or import_name in sys.modules:
    return
spec = importlib.util.find_spec(import_name)        # side effect: imports parent package "app" first

When the script is run as python app/app.py from the parent directory, sys.path[0] is set to app/ (the script's own directory), not the parent. importlib.util.find_spec("app.app") must first import the parent package app to resolve the dotted name — and because app/ is on sys.path, Python resolves app to app/app.py itself (there's no app/__init__.py; a top-level module named app shadows the script). That reimport executes app/app.py again, top-to-bottom, under module name app (not __main__), which calls Dash(use_pages=True) a second time and — because Dash's pages loader doesn't consult sys.modules before exec_module-ing page files — every page-level @callback is appended to dash._callback.GLOBAL_CALLBACK_LIST a second time. find_spec then raises (app "is not a package"), which alias_main_module swallows (except (ImportError, ValueError, OSError): pass), so the bug is silent until the client renders and finds duplicate outputs.

This is the mirror-image of #3818: that fix special-cased uvicorn --reload's dual-process import; this path is a single-process python <subdir>/<same-name>.py launch, which is common enough (app/app.py, src/app.py, any Flask/Dash tutorial's "python app.py" instruction run from one directory up) that it's likely to recur for other users bumping past 4.4.0.

Suggested fix

alias_main_module could avoid the side-effectful find_spec on the dotted name entirely by resolving only the top-level package with importlib.machinery.PathFinder.find_spec(top_level, path=[the parent of module_file's directory]) and comparing paths, without ever importing anything — or by checking top_level not in sys.modules and, if the resulting module's __file__ matches module_file, skip re-registration instead of relying on the find_spec exception path. Alternatively, guard early: skip aliasing entirely when sys.modules[import_name.split(".")[0]] would resolve to the currently-executing __main__ file (i.e., directory name equals script basename).

Versions

  • dash 4.4.1 (also reproduced on 4.5.0rc0 wheel inspection — alias_main_module unchanged)
  • Python 3.12
  • macOS / Linux (path-shadowing mechanism is OS-independent; not tested on Windows)

Workaround

In the entrypoint file, before constructing Dash(...):

import sys
if __name__ == "__main__":
    sys.modules.setdefault("app", sys.modules[__name__])

This pre-seeds sys.modules["app"] so alias_main_module's find_spec returns immediately (first line: if import_name in sys.modules: return — wait, that guard checks import_name i.e. "app.app", not "app"; the workaround instead makes the nested find_spec("app.app") short-circuit because Python's import machinery sees app already in sys.modules and does not re-execute it). No effect under the production gunicorn app:server launch shape (there, caller_name is app, not __main__, so alias_main_module returns at its very first line).

Linguagem predominante
Python
Estrelas
24.4k
Forks
2.3k
Merge médio
2d 23min
PRs com merge (30d)
17

Preparar o ambiente

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de plotly/dash

Todas as issues de plotly/dash

Issues semelhantes

Mais issues de Python

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.