Skip to content

Python: from <external> import name binds to an unrelated project symbol of the same name (exact-match) — false import cycles #2566

Closed

Description

@kogriv

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/codegraph 1.6.2 from npm, which is the current release
  • also reproduced on main at eec9288 (built from source, tsc only)
  • 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:1

The 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

  1. added a commit that references this issue on Oct 15, 2026
  2. danusha2345 commented on Oct 15, 2026

    @danusha2345
    Contributor

    Reproduced on current main (c71a3d0) with the two files as given: analysis.py -> variable stats (report.py) as an imports edge, exact-match 0.9, and the same for from os import path against a path = … in report.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 and findCircularDependencies() returns [] for this repository.

  3. added a commit that references this issue on Oct 15, 2026
  4. kogriv commented on Oct 30, 2026

    @kogriv
    Author

    Confirmed on main at de0a4eb (built from source), with 1.6.2 from npm as the control. Same harness on both builds:

    analysis.py imports report.py defines 1.6.2 main
    from scipy import stats stats = … exact-match edge + cycle none
    from scipy import stats def stats() exact-match edge + cycle none
    from os import path path = … exact-match edge + cycle none
    import scipy.stats as stats (control) stats = … none none
    from scipy import stats (control) result = … none none

    On both builds, report.py → analysis.summary stays an imports edge 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 on main. All 5 that went through the notebook's stats = … 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's research/codegraph_remeasure.md §8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions