From 7c7da99e06a552788b79ac8e2b723d1f9c01f9e4 Mon Sep 17 00:00:00 2001 From: Renn F Date: Sat, 18 Apr 2026 04:27:59 +0200 Subject: [PATCH] Break cyclic import between mypy.types and mypy.expandtype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `InstantiateAliasVisitor` was defined at the bottom of `mypy.types` and relied on a module-level back-import `from mypy.expandtype import ExpandTypeVisitor`. Current comment acknowledges this as "unfortunate". The back-import is load-order-dependent. When an external consumer (e.g. a mypy plugin such as `pydantic.mypy`) imports `mypy.expandtype` before `mypy.types` has finished loading, execution of `mypy/expandtype.py` line 8 re-enters `mypy/types.py`, which at module level then does `from mypy.expandtype import ExpandTypeVisitor` — but `ExpandTypeVisitor` has not been bound on `mypy.expandtype` yet. Under pure-Python mypy this raises ImportError: cannot import name 'ExpandTypeVisitor' from partially initialized module 'mypy.expandtype' Under compiled mypy (mypyc builds) the `AttributeError` is swallowed silently during plugin registration, so the plugin never loads and downstream decorators (`@field_validator`, `@model_validator`, etc.) are reported as `untyped-decorator` with no diagnostic hint. Fix: move `InstantiateAliasVisitor` to `mypy.expandtype` beside its base class, and replace the module-level back-import in `mypy.types` with a function-scoped lazy import at the single usage site (`TypeAliasType._partial_expansion`). No behavioural change — class body is identical and the one call site is semantically preserved. Verified: full `mypy/test/testcheck.py` suite (7833 tests) passes unchanged; `python -c "import pydantic.mypy"` succeeds on both compiled and pure-Python builds; plugin registers correctly when mypy runs. --- mypy/expandtype.py | 13 +++++++++++++ mypy/types.py | 21 ++------------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 5790b717172ac..28f274ab20264 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -660,3 +660,16 @@ def remove_trivial(types: Iterable[Type]) -> list[Type]: if removed_none: return [NoneType()] return [UninhabitedType()] + + +class InstantiateAliasVisitor(ExpandTypeVisitor): + def visit_union_type(self, t: UnionType) -> Type: + # Unlike regular expand_type(), we don't do any simplification for unions, + # not even removing strict duplicates. There are three reasons for this: + # * get_proper_type() is a very hot function, even slightest slow down will + # cause a perf regression + # * We want to preserve this historical behaviour, to avoid possible + # regressions + # * Simplifying unions may (indirectly) call get_proper_type(), causing + # infinite recursion. + return mypy.type_visitor.TypeTranslator.visit_union_type(self, t) diff --git a/mypy/types.py b/mypy/types.py index 40c3839e2efca..4b5ba2727cb1f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -379,6 +379,8 @@ def _expand_once(self) -> Type: ): mapping[tvar.id] = sub + from mypy.expandtype import InstantiateAliasVisitor + return self.alias.target.accept(InstantiateAliasVisitor(mapping)) @property @@ -4441,22 +4443,3 @@ def write_type_map(data: WriteBuffer, value: dict[str, Type]) -> None: for key in sorted(value): write_str_bare(data, key) value[key].write(data) - - -# This cyclic import is unfortunate, but to avoid it we would need to move away all uses -# of get_proper_type() from types.py. Majority of them have been removed, but few remaining -# are quite tricky to get rid of, but ultimately we want to do it at some point. -from mypy.expandtype import ExpandTypeVisitor - - -class InstantiateAliasVisitor(ExpandTypeVisitor): - def visit_union_type(self, t: UnionType) -> Type: - # Unlike regular expand_type(), we don't do any simplification for unions, - # not even removing strict duplicates. There are three reasons for this: - # * get_proper_type() is a very hot function, even slightest slow down will - # cause a perf regression - # * We want to preserve this historical behaviour, to avoid possible - # regressions - # * Simplifying unions may (indirectly) call get_proper_type(), causing - # infinite recursion. - return TypeTranslator.visit_union_type(self, t)