Repository navigation
Description
Summary
In Python, from <external module> import <name> gets an imports edge to an unrelated project symbol that happens to have the same name: a top-level variable or function in another file. The edge has resolvedBy: "exact-match" and confidence 0.9. If that other file imports the first one, which is common when it is a script using the package, findCircularDependencies() reports a cycle that does not exist. callers <name> lists the importing file as a caller.
This is the Python counterpart of #2101 (TypeScript, closed via #2120/#2128). The 1.6.2 fix does not cover Python.
Environment
@colbymchenry/codegraph1.6.2 from npm, which is the current release- also reproduced on
mainat eec9288 (built from source,tsconly) - Linux, Node 24
Minimal reproduction (2 files)
# analysis.py
from scipy import stats
def summary(values):
return stats.describe(values)# report.py
from analysis import summary
stats = summary([1, 2, 3])scipy does not need to be installed.
$ codegraph init -y .
$ codegraph callers stats
Callers of "stats" (1):
variable stats (python) — report.py:3
file analysis.py [imports]
analysis.py:1The edges table:
file:analysis.py -> variable stats (report.py) imports {"confidence":0.9,"resolvedBy":"exact-match","refName":"stats"}
file:report.py -> function summary (analysis.py) imports {"confidence":0.9,"resolvedBy":"import","refName":"summary"}
CodeGraph.open(dir).findCircularDependencies() → [["analysis.py","report.py"]]
Expected: no edge from analysis.py to report.py, because stats comes from scipy and no cycle exists.
Variants (1.6.2 and main eec9288 give the same results)
analysis.py imports |
report.py defines |
cycle reported |
|---|---|---|
from scipy import stats |
stats = ... (variable) |
yes |
from scipy import stats |
def stats(): ... |
yes |
from os import path (stdlib) |
path = ... |
yes |
import scipy.stats as stats |
stats = ... |
no |
from scipy import stats |
result = ... (control) |
no |
The problem is therefore the from X import name form with an external X, whether X is a third-party package or the stdlib.
Relation to #2409 / #1921
#2409 (import logging → app/core/logging.py) is a different path through the resolver. There the edge targets a file and has resolvedBy: "import" (filename-suffix match). Here it targets a symbol and has resolvedBy: "exact-match".
I built the head of open PR #1921 (b9840f6), and it removes this edge as well: all three positive rows above give [], and the two controls stay []. The PR description mentions only the file-suffix case. Its tests may not pin this form, so this repro could be useful as a regression case if the PR lands.
Where it was found
On a real 280-file Python repository (a package plus research scripts), 1.6.2 reports 25 import cycles. 5 of them are this defect: one script defines a top-level stats, and five package modules do from scipy import stats. On #1921's head these 5 are gone, and the number of true cycles found does not change (16 of 45 in both builds).
Not checked: other languages; Windows; from X import a as b with a local b; whether callees / impact / affected follow the edge (I expect they do, as in #2101, but did not verify).
Activity
test(python): pin colbymchenry#2566 — `from <external> import name` n…
danusha2345 commentedon Oct 15, 2026
Reproduced on current
main(c71a3d0) with the two files as given:analysis.py -> variable stats (report.py)as animportsedge,exact-match0.9, and the same forfrom os import pathagainst apath = …inreport.py.The open PR #1921 (@inth3shadows — Python imports resolved from the package roots the project has) already carries the rule this needs: a name imported from a module no package root of the project provides names nothing in the project, so the import stays unresolved instead of falling to name matching. With that branch applied on top of
main, both edges are gone andfindCircularDependencies()returns[]for this repository.test(python): pin colbymchenry#2566 — `from <external> import name` n…
kogriv commentedon Oct 30, 2026
Confirmed on
mainat de0a4eb (built from source), with 1.6.2 from npm as the control. Same harness on both builds:analysis.pyimportsreport.pydefinesmainfrom scipy import statsstats = …exact-matchedge + cyclefrom scipy import statsdef stats()exact-matchedge + cyclefrom os import pathpath = …exact-matchedge + cycleimport scipy.stats as stats(control)stats = …from scipy import stats(control)result = …On both builds,
report.py → analysis.summarystays animportsedge in every row, so the fix does not drop the import that should resolve.On the 280-file tree from the original report,
findCircularDependencies()gives 25 cycles on 1.6.2 and 22 onmain. All 5 that went through the notebook'sstats = …are gone. The cycles that match a real import cycle stay at 16 on both builds, so keeping function-local imports out of the file's mappings (7c843e2) did not cost any real cycles.Thanks to @inth3shadows for the fix and the pinned test, and to @danusha2345 for confirming it before the merge.
Linux x64, Node 22,
DO_NOT_TRACK=1. Raw results and the expectations, written before the run, are public in the measuring project'sresearch/codegraph_remeasure.md§8.