From 7a07960ba14a00b9bfce07a454f09421ac917251 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 May 2026 23:08:06 +0100 Subject: [PATCH 1/8] Add limited implementation --- mypy/semanal.py | 96 ++++++++-------------- mypy/type_visitor.py | 2 +- mypy/typeanal.py | 25 +++++- mypy/types.py | 1 + test-data/unit/check-python313.test | 5 ++ test-data/unit/check-typevar-defaults.test | 65 +++++++++++++++ 6 files changed, 126 insertions(+), 68 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index a958043fa35c2..44f97fa575991 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1899,6 +1899,7 @@ def analyze_type_param( allow_param_spec_literals=type_param.kind == PARAM_SPEC_KIND, allow_tuple_literal=type_param.kind == PARAM_SPEC_KIND, allow_unpack=type_param.kind == TYPE_VAR_TUPLE_KIND, + analyzing_tvar_def=True, ) if default is None: default = PlaceholderType(None, [], context.line) @@ -1985,18 +1986,10 @@ def analyze_class(self, defn: ClassDef) -> None: self.check_type_alias_bases(bases) - for tvd in tvar_defs: - if isinstance(tvd, TypeVarType) and any( - has_placeholder(t) for t in [tvd.upper_bound] + tvd.values - ): - # Some type variable bounds or values are not ready, we need - # to re-analyze this class. - self.defer() - if has_placeholder(tvd.default): - # Placeholder values in TypeVarLikeTypes may get substituted in. - # Defer current target until they are ready. - self.mark_incomplete(defn.name, defn) - return + if any(has_placeholder(tvd) for tvd in tvar_defs): + # Some type variable bounds or values are not ready, we need + # to re-analyze this class. + self.defer() self.analyze_class_keywords(defn) bases_result = self.analyze_base_classes(bases) @@ -4167,6 +4160,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # may appear in nested positions), therefore use becomes_typeinfo=True. self.mark_incomplete(lvalue.name, rvalue, becomes_typeinfo=True) return True + + if any(has_placeholder(tv) for tv in alias_tvars): + self.defer() + self.add_type_alias_deps(depends_on) check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg, context=s) # When this type alias gets "inlined", the Any is not explicit anymore, @@ -4220,7 +4217,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # An alias gets updated. updated = False if isinstance(existing.node, TypeAlias): - if existing.node.target != res: + if existing.node.target != res or existing.node.alias_tvars != alias_tvars: # Copy expansion to the existing alias, this matches how we update base classes # for a TypeInfo _in place_ if there are nested placeholders. existing.node.target = res @@ -4990,6 +4987,7 @@ def get_typevarlike_argument( allow_unbound_tvars=allow_unbound_tvars, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, + analyzing_tvar_def=param_name == "default", ) if analyzed is None: # Type variables are special: we need to place them in the symbol table @@ -4999,15 +4997,21 @@ def get_typevarlike_argument( # class Custom(Generic[T]): # ... analyzed = PlaceholderType(None, [], context.line) - typ = get_proper_type(analyzed) - if report_invalid_typevar_arg and isinstance(typ, AnyType) and typ.is_from_error: - self.fail( - message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name), - param_value, - ) + if report_invalid_typevar_arg: + if ( + isinstance(analyzed, ProperType) + and isinstance(analyzed, AnyType) + and analyzed.is_from_error + ): + self.fail( + message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format( + typevarlike_name, param_name + ), + param_value, + ) # Note: we do not return 'None' here -- we want to continue # using the AnyType. - return typ + return analyzed except TypeTranslationError: if report_invalid_typevar_arg: self.fail( @@ -5715,10 +5719,8 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: # Now go through all new variables and temporary replace all tvars that still # refer to some placeholders. We defer the whole alias and will revisit it again, # as well as all its dependents. - for i, tv in enumerate(alias_tvars): - if has_placeholder(tv): - self.mark_incomplete(s.name.name, s.value, becomes_typeinfo=True) - alias_tvars[i] = self._trivial_typevarlike_like(tv) + if any(has_placeholder(tv) for tv in alias_tvars): + self.defer() self.add_type_alias_deps(depends_on) check_for_explicit_any( @@ -5786,46 +5788,6 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: finally: self.pop_type_args(s.type_args) - def _trivial_typevarlike_like(self, tv: TypeVarLikeType) -> TypeVarLikeType: - object_type = self.named_type("builtins.object") - if isinstance(tv, TypeVarType): - return TypeVarType( - tv.name, - tv.fullname, - tv.id, - values=[], - upper_bound=object_type, - default=AnyType(TypeOfAny.from_omitted_generics), - variance=tv.variance, - line=tv.line, - column=tv.column, - ) - elif isinstance(tv, TypeVarTupleType): - tuple_type = self.named_type("builtins.tuple", [object_type]) - return TypeVarTupleType( - tv.name, - tv.fullname, - tv.id, - upper_bound=tuple_type, - tuple_fallback=tuple_type, - default=AnyType(TypeOfAny.from_omitted_generics), - line=tv.line, - column=tv.column, - ) - elif isinstance(tv, ParamSpecType): - return ParamSpecType( - tv.name, - tv.fullname, - tv.id, - flavor=tv.flavor, - upper_bound=object_type, - default=AnyType(TypeOfAny.from_omitted_generics), - line=tv.line, - column=tv.column, - ) - else: - assert False, f"Unknown TypeVarLike: {tv!r}" - # # Expressions # @@ -7711,6 +7673,7 @@ def expr_to_analyzed_type( allow_unbound_tvars: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, + analyzing_tvar_def: bool = False, ) -> Type | None: if isinstance(expr, CallExpr): # This is a legacy syntax intended mostly for Python 2, we keep it for @@ -7742,6 +7705,7 @@ def expr_to_analyzed_type( allow_unbound_tvars=allow_unbound_tvars, allow_param_spec_literals=allow_param_spec_literals, allow_unpack=allow_unpack, + analyzing_tvar_def=analyzing_tvar_def, ) def analyze_type_expr(self, expr: Expression) -> None: @@ -7769,6 +7733,7 @@ def type_analyzer( prohibit_self_type: str | None = None, prohibit_special_class_field_types: str | None = None, allow_type_any: bool = False, + analyzing_tvar_def: bool = False, ) -> TypeAnalyser: if tvar_scope is None: tvar_scope = self.tvar_scope @@ -7790,6 +7755,7 @@ def type_analyzer( prohibit_self_type=prohibit_self_type, prohibit_special_class_field_types=prohibit_special_class_field_types, allow_type_any=allow_type_any, + analyzing_tvar_def=analyzing_tvar_def, ) tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic()) tpan.global_scope = not self.type and not self.function_stack @@ -7816,6 +7782,7 @@ def anal_type( prohibit_self_type: str | None = None, prohibit_special_class_field_types: str | None = None, allow_type_any: bool = False, + analyzing_tvar_def: bool = False, ) -> Type | None: """Semantically analyze a type. @@ -7853,6 +7820,7 @@ def anal_type( prohibit_self_type=prohibit_self_type, prohibit_special_class_field_types=prohibit_special_class_field_types, allow_type_any=allow_type_any, + analyzing_tvar_def=analyzing_tvar_def, ) tag = self.track_incomplete_refs() typ = typ.accept(a) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 1b38481ba0004..58118f2554cb3 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -595,7 +595,7 @@ def visit_type_alias_type(self, t: TypeAliasType, /) -> bool: elif t in self.seen_aliases: return self.default self.seen_aliases.add(t) - return get_proper_type(t).accept(self) + return get_proper_type(t).accept(self) or self.query_types(t.args) def query_types(self, types: list[Type] | tuple[Type, ...]) -> bool: """Perform a query for a sequence of types using the strategy to combine the results.""" diff --git a/mypy/typeanal.py b/mypy/typeanal.py index db56256192625..5e61af9b9ef33 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -222,6 +222,7 @@ def __init__( allowed_alias_tvars: list[TypeVarLikeType] | None = None, allow_type_any: bool = False, alias_type_params_names: list[str] | None = None, + analyzing_tvar_def: bool = False, ) -> None: self.api = api self.fail_func = api.fail @@ -268,6 +269,7 @@ def __init__( self.allow_type_any = allow_type_any self.allow_type_var_tuple = False self.allow_unpack = allow_unpack + self.analyzing_tvar_def = analyzing_tvar_def def lookup_qualified( self, name: str, ctx: Context, suppress_errors: bool = False @@ -483,6 +485,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) unexpanded_type=t, disallow_any=disallow_any, empty_tuple_index=t.empty_tuple_index, + analyzing_tvar_def=self.analyzing_tvar_def, ) # The only case where instantiate_type_alias() can return an incorrect instance is # when it is top-level instance, so no need to recurse. @@ -500,6 +503,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) options=self.options, use_generic_error=True, unexpanded_type=t, + analyzing_tvar_def=self.analyzing_tvar_def, ) if node.eager: res = get_proper_type(res) @@ -883,6 +887,7 @@ def analyze_type_with_type_info( self.note, disallow_any=self.options.disallow_any_generics and not self.is_typeshed_stub, options=self.options, + analyzing_tvar_def=self.analyzing_tvar_def, ) tup = info.tuple_type @@ -899,6 +904,7 @@ def analyze_type_with_type_info( ctx, self.options, use_standard_error=True, + analyzing_tvar_def=self.analyzing_tvar_def, ) return tup.copy_modified( items=self.anal_array(tup.items, allow_unpack=True), fallback=instance @@ -917,6 +923,7 @@ def analyze_type_with_type_info( ctx, self.options, use_standard_error=True, + analyzing_tvar_def=self.analyzing_tvar_def, ) # Create a named TypedDictType return td.copy_modified( @@ -2065,6 +2072,7 @@ def fix_instance( options: Options, use_generic_error: bool = False, unexpanded_type: Type | None = None, + analyzing_tvar_def: bool = False, ) -> None: """Fix a malformed instance by replacing all type arguments with TypeVar default or Any. @@ -2088,7 +2096,7 @@ def fix_instance( if tv is None: continue if arg is None: - if tv.has_default(): + if tv.has_default() and not analyzing_tvar_def: arg = tv.default else: if any_type is None: @@ -2120,6 +2128,7 @@ def instantiate_type_alias( disallow_any: bool = False, use_standard_error: bool = False, empty_tuple_index: bool = False, + analyzing_tvar_def: bool = False, ) -> Type: """Create an instance of a (generic) type alias from alias node and type arguments. @@ -2168,6 +2177,7 @@ def instantiate_type_alias( disallow_any=disallow_any, fail=fail, unexpanded_type=unexpanded_type, + analyzing_tvar_def=analyzing_tvar_def, ) if max_tv_count == 0 and act_len == 0: if no_args: @@ -2233,7 +2243,15 @@ def instantiate_type_alias( ) fail(msg, ctx, code=codes.TYPE_ARG) args = [] - return set_any_tvars(node, args, ctx.line, ctx.column, options, from_error=True) + return set_any_tvars( + node, + args, + ctx.line, + ctx.column, + options, + from_error=True, + analyzing_tvar_def=analyzing_tvar_def, + ) elif node.tvar_tuple_index is not None: # We also need to check if we are not performing a type variable tuple split. unpack = find_unpack_in_list(args) @@ -2276,6 +2294,7 @@ def set_any_tvars( special_form: bool = False, fail: MsgCallback | None = None, unexpanded_type: Type | None = None, + analyzing_tvar_def: bool = False, ) -> TypeAliasType: if from_error or disallow_any: type_of_any = TypeOfAny.from_error @@ -2292,7 +2311,7 @@ def set_any_tvars( if tv is None: continue if arg is None: - if tv.has_default(): + if tv.has_default() and not analyzing_tvar_def: arg = tv.default else: arg = any_type diff --git a/mypy/types.py b/mypy/types.py index 40c3839e2efca..8baa0044de890 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -699,6 +699,7 @@ def __eq__(self, other: object) -> bool: self.id == other.id and self.upper_bound == other.upper_bound and self.values == other.values + and self.default == other.default ) def serialize(self) -> JsonDict: diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 8a80977fb22a9..7976a80b789ac 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -337,3 +337,8 @@ type Result[T, E] = Ok[T, E] | Err[E, T] class Bar[U]: def foo(data: U, cond: bool) -> Result[U, str]: return Ok(data) if cond else Err("Error") + +[case testRecursiveAliasTypeVarDefaultNewStyle] +type A[T = A] = int +a: A +reveal_type(a) # N: Revealed type is "builtins.int" diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 535d882ccf3c4..7463d0ad05270 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -937,3 +937,68 @@ reveal_type(D) # N: Revealed type is "def [T2 = Any, T1 = Any] () -> __main__.D d: D reveal_type(d) # N: Revealed type is "__main__.D[Any, Any]" [builtins fixtures/tuple.pyi] + +[case testRecursiveTypeVarDefaultBasic] +from typing import TypeVar, Generic + +class C(Generic["T"]): + pass + +T = TypeVar("T", bound=C, default=C) + +c: C +reveal_type(c) # N: Revealed type is "__main__.C[__main__.C[Any]]" + +[case testRecursiveTypeVarDefaultMutual] +from typing import TypeVar, Generic + +class C(Generic["T"]): + pass + +class D(Generic["S"]): + pass + +T = TypeVar("T", default=D) +S = TypeVar("S", default=C) + +c: C +d: D +reveal_type(c) # N: Revealed type is "__main__.C[__main__.D[Any]]" +reveal_type(d) # N: Revealed type is "__main__.D[__main__.C[Any]]" + +[case testNonRecursiveSimpleTypeVarDefault] +from typing import TypeVar, Generic + +S = TypeVar("S", default="Parent") +class Child(Generic[S]): ... + +T = TypeVar("T", default=int) +class Parent(Generic[T]): ... + +reveal_type(Child()) # N: Revealed type is "__main__.Child[__main__.Parent[builtins.int]]" + +[case testRecursiveTypeVarDefaultClassAndAlias] +from typing import Generic, TypeVar, Union + +T = TypeVar("T", default="Pattern") + +class Trait(Generic[T]): + pass + +class Pattern1(Trait): + pass + +Pattern = Union[Pattern1, None] + +reveal_type(Trait()) # N: Revealed type is "__main__.Trait[__main__.Pattern1 | None]" +[builtins fixtures/tuple.pyi] + +[case testRecursiveTypeVarDefaultOnlyAlias] +from typing import TypeVar + +T = TypeVar("T", default="A") + +A = list[T] +a: A +reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" +[builtins fixtures/tuple.pyi] From cddf39f07575e3161c4c5700e08e1e696327d543 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 May 2026 18:19:15 +0100 Subject: [PATCH 2/8] Support non-recursive incomplete defaults --- mypy/checker.py | 3 ++ mypy/checkexpr.py | 4 +- mypy/nodes.py | 15 +++++- mypy/semanal.py | 72 ++++++++++++++++++++++----- mypy/semanal_shared.py | 5 ++ mypy/typeanal.py | 108 +++++++++++++++++++++++++++++++++-------- 6 files changed, 172 insertions(+), 35 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 80402e71dce69..53227c0c4a814 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8529,6 +8529,9 @@ def lookup_fully_qualified_or_none(self, fullname: str, /) -> SymbolTableNode | except KeyError: return None + def record_fixed_type(self, fixed: TypeInfo | TypeAlias) -> None: + pass + def fail( self, msg: str, diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 48ea7ab51f61b..8ecd7eefce251 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4915,7 +4915,7 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: if tapp.expr.node.python_3_12_type_alias: return self.type_alias_type_type() # Subscription of a (generic) alias in runtime context, expand the alias. - item = instantiate_type_alias( + item, _ = instantiate_type_alias( tapp.expr.node, tapp.types, self.chk.fail, @@ -4992,7 +4992,7 @@ class LongName(Generic[T]): ... self.chk.options, disallow_any=disallow_any, fail=self.msg.fail, - ) + )[0] ) if isinstance(item, Instance): # Normally we get a callable type (or overloaded) with .is_type_obj() true diff --git a/mypy/nodes.py b/mypy/nodes.py index 32a694560b24b..f0216004a45b7 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3145,7 +3145,15 @@ class TypeVarLikeExpr(SymbolNode, Expression): Note that they are constructed by the semantic analyzer. """ - __slots__ = ("_name", "_fullname", "upper_bound", "default", "variance", "is_new_style") + __slots__ = ( + "_name", + "_fullname", + "upper_bound", + "default", + "variance", + "is_new_style", + "default_depends", + ) _name: str _fullname: str @@ -3178,6 +3186,7 @@ def __init__( self.default = default self.variance = variance self.is_new_style = is_new_style + self.default_depends: set[TypeInfo | TypeAlias] | None = None @property def name(self) -> str: @@ -3655,6 +3664,7 @@ class is generic then it will be a type constructor of higher kind. "is_type_check_only", "deprecated", "type_object_type", + "default_depends", ) _fullname: str # Fully qualified name @@ -3877,6 +3887,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.is_type_check_only = False self.deprecated = None self.type_object_type = None + self.default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} def add_type_vars(self) -> None: self.has_type_var_tuple_type = False @@ -4542,6 +4553,7 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here "eager", "tvar_tuple_index", "python_3_12_type_alias", + "default_depends", ) __match_args__ = ("name", "target", "alias_tvars", "no_args") @@ -4574,6 +4586,7 @@ def __init__( self.eager = eager self.python_3_12_type_alias = python_3_12_type_alias self.tvar_tuple_index = None + self.default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} for i, t in enumerate(alias_tvars): if isinstance(t, mypy.types.TypeVarTupleType): self.tvar_tuple_index = i diff --git a/mypy/semanal.py b/mypy/semanal.py index 26a7e3953f7b4..e51ce599009b2 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -538,6 +538,7 @@ def __init__( # [b.py] # import foo.bar self.transitive_submodule_imports: dict[str, set[str]] = {} + self.types_fixed: set[TypeInfo | TypeAlias] | None = None # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties @@ -1883,6 +1884,7 @@ def analyze_type_param( upper_bound = self.named_type("builtins.tuple", [self.object_type()]) else: upper_bound = self.object_type() + self.types_fixed = None if type_param.default: default = self.anal_type( type_param.default, @@ -1904,6 +1906,8 @@ def analyze_type_param( default = self.check_typevartuple_default(default, type_param.default) else: default = AnyType(TypeOfAny.from_omitted_generics) + default_depends = self.types_fixed + self.types_fixed = None if type_param.kind == TYPE_VAR_KIND: values: list[Type] = [] if type_param.values: @@ -1916,7 +1920,7 @@ def analyze_type_param( values.append(AnyType(TypeOfAny.from_error)) else: values.append(analyzed) - return TypeVarExpr( + tv = TypeVarExpr( name=type_param.name, fullname=fullname, values=values, @@ -1927,7 +1931,7 @@ def analyze_type_param( line=context.line, ) elif type_param.kind == PARAM_SPEC_KIND: - return ParamSpecExpr( + tv = ParamSpecExpr( name=type_param.name, fullname=fullname, upper_bound=upper_bound, @@ -1938,7 +1942,7 @@ def analyze_type_param( else: assert type_param.kind == TYPE_VAR_TUPLE_KIND tuple_fallback = self.named_type("builtins.tuple", [self.object_type()]) - return TypeVarTupleExpr( + tv = TypeVarTupleExpr( name=type_param.name, fullname=fullname, upper_bound=upper_bound, @@ -1947,6 +1951,8 @@ def analyze_type_param( is_new_style=True, line=context.line, ) + tv.default_depends = default_depends + return tv def pop_type_args(self, type_args: list[TypeParam] | None) -> None: if not type_args: @@ -1973,11 +1979,15 @@ def analyze_class(self, defn: ClassDef) -> None: self.infer_metaclass_and_bases_from_compat_helpers(defn) bases = defn.base_type_exprs - bases, tvar_defs, is_protocol = self.clean_up_bases_and_infer_type_variables( - defn, bases, context=defn + bases, tvar_defs, is_protocol, declared_tvars = ( + self.clean_up_bases_and_infer_type_variables(defn, bases, context=defn) ) self.check_type_alias_bases(bases) + default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} + for _, tv in declared_tvars: + if tv.default_depends is not None: + default_depends[tv.fullname] = tv.default_depends if any(has_placeholder(tvd) for tvd in tvar_defs): # Some type variable bounds or values are not ready, we need @@ -2010,14 +2020,18 @@ def analyze_class(self, defn: ClassDef) -> None: if defn.info: self.setup_type_vars(defn, tvar_defs) self.setup_alias_type_vars(defn) + defn.info.default_depends = default_depends return if self.analyze_namedtuple_classdef(defn, tvar_defs): + if defn.info: + defn.info.default_depends = default_depends return # Create TypeInfo for class now that base classes and the MRO can be calculated. self.prepare_class_def(defn) self.setup_type_vars(defn, tvar_defs) + defn.info.default_depends = default_depends if base_error: defn.info.fallback_to_any = True if any_meta: @@ -2257,7 +2271,7 @@ def analyze_class_decorator_common(self, defn: ClassDef, decorator: Expression) def clean_up_bases_and_infer_type_variables( self, defn: ClassDef, base_type_exprs: list[Expression], context: Context - ) -> tuple[list[Expression], list[TypeVarLikeType], bool]: + ) -> tuple[list[Expression], list[TypeVarLikeType], bool, list[tuple[str, TypeVarLikeExpr]]]: """Remove extra base classes such as Generic and infer type vars. For example, consider this class: @@ -2349,7 +2363,7 @@ class Foo(Bar, Generic[T]): ... defn.removed_base_type_exprs.append(defn.base_type_exprs[i]) del base_type_exprs[i] tvar_defs = self.tvar_defs_from_tvars(declared_tvars, context) - return base_type_exprs, tvar_defs, is_protocol + return base_type_exprs, tvar_defs, is_protocol, declared_tvars def analyze_class_typevar_declaration( self, base: Type, has_type_var_tuple: bool @@ -3973,7 +3987,9 @@ def analyze_alias( declared_type_vars: TypeVarLikeList | None = None, all_declared_type_params_names: list[str] | None = None, python_3_12_type_alias: bool = False, - ) -> tuple[Type | None, list[TypeVarLikeType], set[str], bool]: + ) -> tuple[ + Type | None, list[TypeVarLikeType], set[str], bool, dict[str, set[TypeAlias | TypeInfo]] + ]: """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type, a list of type variables for generic aliases, @@ -3993,7 +4009,7 @@ def analyze_alias( self.fail( "Invalid type alias: expression is not a valid type", rvalue, code=codes.VALID_TYPE ) - return None, [], set(), False + return None, [], set(), False, {} found_type_vars = self.find_type_var_likes(typ) namespace = self.qualified_name(name) @@ -4032,7 +4048,11 @@ def analyze_alias( new_tvar_defs.append(td) indexed = bool(isinstance(typ, UnboundType) and (typ.args or typ.empty_tuple_index)) - return analyzed, new_tvar_defs, depends_on, indexed + default_depends = {} + for _, tv in alias_type_vars: + if tv.default_depends is not None: + default_depends[tv.fullname] = tv.default_depends + return analyzed, new_tvar_defs, depends_on, indexed, default_depends def is_pep_613(self, s: AssignmentStmt) -> bool: if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType): @@ -4132,9 +4152,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: alias_tvars: list[TypeVarLikeType] = [] depends_on: set[str] = set() indexed = False + default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} else: tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, indexed = self.analyze_alias( + res, alias_tvars, depends_on, indexed, default_depends = self.analyze_alias( lvalue.name, rvalue, allow_placeholder=True, @@ -4199,6 +4220,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: eager=eager, python_3_12_type_alias=pep_695, ) + alias_node.default_depends = default_depends if isinstance(s.rvalue, (IndexExpr, CallExpr, OpExpr)): # Note: CallExpr is for "void = type(None)" and OpExpr is for "X | Y" union syntax. if not isinstance(s.rvalue.analyzed, TypeAliasExpr): @@ -4218,6 +4240,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # Copy expansion to the existing alias, this matches how we update base classes # for a TypeInfo _in place_ if there are nested placeholders. existing.node.target = res + existing.node.default_depends = default_depends existing.node.alias_tvars = alias_tvars existing.node.no_args = no_args updated = True @@ -4750,6 +4773,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values = call.arg_kinds[1:].count(ARG_POS) values = self.analyze_value_types(call.args[1 : 1 + n_values]) + self.types_fixed = None res = self.process_typevar_parameters( call.args[1 + n_values :], call.arg_names[1 + n_values :], @@ -4757,6 +4781,8 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values, s, ) + default_depends = self.types_fixed + self.types_fixed = None if res is None: return False variance, upper_bound, default = res @@ -4797,6 +4823,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: type_var = TypeVarExpr( name, self.qualified_name(name), values, upper_bound, default, variance ) + type_var.default_depends = default_depends type_var.line = call.line call.analyzed = type_var updated = True @@ -4810,6 +4837,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: call.analyzed.upper_bound = upper_bound call.analyzed.values = values call.analyzed.default = default + call.analyzed.default_depends = default_depends if any(has_placeholder(v) for v in values): self.process_placeholder(None, "TypeVar values", s, force_progress=updated) elif has_placeholder(upper_bound): @@ -4962,6 +4990,11 @@ def process_typevar_parameters( variance = INVARIANT return variance, upper_bound, default + def record_fixed_type(self, fixed: TypeInfo | TypeAlias) -> None: + if self.types_fixed is None: + self.types_fixed = set() + self.types_fixed.add(fixed) + def get_typevarlike_argument( self, typevarlike_name: str, @@ -4973,7 +5006,7 @@ def get_typevarlike_argument( allow_param_spec_literals: bool = False, allow_unpack: bool = False, report_invalid_typevar_arg: bool = True, - ) -> ProperType | None: + ) -> Type | None: try: # We want to use our custom error message below, so we suppress # the default error message for invalid types here. @@ -5053,6 +5086,7 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: if n_values != 0: self.fail('Too many positional arguments for "ParamSpec"', s) + self.types_fixed = None default: Type = AnyType(TypeOfAny.from_omitted_generics) for param_value, param_name in zip( call.args[1 + n_values :], call.arg_names[1 + n_values :] @@ -5077,6 +5111,8 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: "The variance and bound arguments to ParamSpec do not have defined semantics yet", s, ) + default_depends = self.types_fixed + self.types_fixed = None # PEP 612 reserves the right to define bound, covariant and contravariant arguments to # ParamSpec in a later PEP. If and when that happens, we should do something @@ -5087,12 +5123,14 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: name, self.qualified_name(name), self.object_type(), default, INVARIANT ) paramspec_var.line = call.line + paramspec_var.default_depends = default_depends call.analyzed = paramspec_var updated = True else: assert isinstance(call.analyzed, ParamSpecExpr) updated = default != call.analyzed.default call.analyzed.default = default + call.analyzed.default_depends = default_depends if has_placeholder(default): self.process_placeholder(None, "ParamSpec default", s, force_progress=updated) @@ -5115,6 +5153,7 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: self.fail('Too many positional arguments for "TypeVarTuple"', s) default: Type = AnyType(TypeOfAny.from_omitted_generics) + self.types_fixed = None for param_value, param_name in zip( call.args[1 + n_values :], call.arg_names[1 + n_values :] ): @@ -5133,6 +5172,9 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: else: self.fail(f'Unexpected keyword argument "{param_name}" for "TypeVarTuple"', s) + default_depends = self.types_fixed + self.types_fixed = None + name = self.extract_typevarlike_name(s, call) if name is None: return False @@ -5150,12 +5192,14 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: INVARIANT, ) typevartuple_var.line = call.line + typevartuple_var.default_depends = default_depends call.analyzed = typevartuple_var updated = True else: assert isinstance(call.analyzed, TypeVarTupleExpr) updated = default != call.analyzed.default call.analyzed.default = default + call.analyzed.default_depends = default_depends if has_placeholder(default): self.process_placeholder(None, "TypeVarTuple default", s, force_progress=updated) @@ -5686,7 +5730,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: return tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, indexed = self.analyze_alias( + res, alias_tvars, depends_on, indexed, default_depends = self.analyze_alias( s.name.name, s.value.expr(), allow_placeholder=True, @@ -5743,6 +5787,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: eager=eager, python_3_12_type_alias=True, ) + alias_node.default_depends = default_depends s.alias_node = alias_node if ( @@ -5759,6 +5804,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: # Copy expansion to the existing alias, this matches how we update base classes # for a TypeInfo _in place_ if there are nested placeholders. existing.node.target = res + existing.node.default_depends = default_depends existing.node.alias_tvars = alias_tvars updated = True # Invalidate recursive status cache in case it was previously set. diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index a85d4ed00b5e6..c682a3eeb6fbf 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -24,6 +24,7 @@ SymbolNode, SymbolTable, SymbolTableNode, + TypeAlias, TypeInfo, ) from mypy.plugin import SemanticAnalyzerPluginInterface @@ -84,6 +85,10 @@ def lookup_fully_qualified(self, fullname: str, /) -> SymbolTableNode: def lookup_fully_qualified_or_none(self, fullname: str, /) -> SymbolTableNode | None: raise NotImplementedError + @abstractmethod + def record_fixed_type(self, fixed: TypeInfo | TypeAlias) -> None: + raise NotImplementedError + @abstractmethod def fail( self, diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 5e61af9b9ef33..95e096b3d312e 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -475,7 +475,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) an_args = self.pack_paramspec_args(an_args, t.empty_tuple_index) disallow_any = self.options.disallow_any_generics and not self.is_typeshed_stub - res = instantiate_type_alias( + res, used_default = instantiate_type_alias( node, an_args, self.fail, @@ -487,6 +487,9 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) empty_tuple_index=t.empty_tuple_index, analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default and isinstance(res, TypeAliasType): + assert res.alias is not None + self.api.record_fixed_type(res.alias) # The only case where instantiate_type_alias() can return an incorrect instance is # when it is top-level instance, so no need to recurse. if ( @@ -495,7 +498,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) and not (self.defining_alias and self.nesting_level == 0) and not validate_instance(res, self.fail, t.empty_tuple_index) ): - fix_instance( + used_default = fix_instance( res, self.fail, self.note, @@ -505,6 +508,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) unexpanded_type=t, analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + self.api.record_fixed_type(res.type) if node.eager: res = get_proper_type(res) return res @@ -881,7 +886,7 @@ def analyze_type_with_type_info( if not (self.defining_alias and self.nesting_level == 0) and not validate_instance( instance, self.fail, empty_tuple_index ): - fix_instance( + used_default = fix_instance( instance, self.fail, self.note, @@ -889,13 +894,15 @@ def analyze_type_with_type_info( options=self.options, analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + self.api.record_fixed_type(info) tup = info.tuple_type if tup is not None: # The class has a Tuple[...] base class so it will be # represented as a tuple type. if info.special_alias: - return instantiate_type_alias( + res, used_default = instantiate_type_alias( info.special_alias, # TODO: should we allow NamedTuples generic in ParamSpec? self.anal_array(args, allow_unpack=True), @@ -906,6 +913,9 @@ def analyze_type_with_type_info( use_standard_error=True, analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + self.api.record_fixed_type(info) + return res return tup.copy_modified( items=self.anal_array(tup.items, allow_unpack=True), fallback=instance ) @@ -914,7 +924,7 @@ def analyze_type_with_type_info( # The class has a TypedDict[...] base class so it will be # represented as a typeddict type. if info.special_alias: - return instantiate_type_alias( + res, used_default = instantiate_type_alias( info.special_alias, # TODO: should we allow TypedDicts generic in ParamSpec? self.anal_array(args, allow_unpack=True), @@ -925,6 +935,9 @@ def analyze_type_with_type_info( use_standard_error=True, analyzing_tvar_def=self.analyzing_tvar_def, ) + if self.analyzing_tvar_def and used_default: + self.api.record_fixed_type(info) + return res # Create a named TypedDictType return td.copy_modified( item_types=self.anal_array(list(td.items.values())), fallback=instance @@ -2073,11 +2086,12 @@ def fix_instance( use_generic_error: bool = False, unexpanded_type: Type | None = None, analyzing_tvar_def: bool = False, -) -> None: +) -> bool: """Fix a malformed instance by replacing all type arguments with TypeVar default or Any. Also emit a suitable error if this is not due to implicit Any's. """ + used_default = False arg_count = len(t.args) min_tv_count = sum(not tv.has_default() for tv in t.type.defn.type_vars) max_tv_count = len(t.type.type_vars) @@ -2096,15 +2110,24 @@ def fix_instance( if tv is None: continue if arg is None: - if tv.has_default() and not analyzing_tvar_def: + use_any = False + if tv.has_default(): arg = tv.default + if analyzing_tvar_def: + used_default = True + if is_typevar_default_recursive(tv.fullname, t.type): + use_any = True else: + use_any = True + if use_any: if any_type is None: fullname = None if use_generic_error else t.type.fullname any_type = get_omitted_any( disallow_any, fail, note, t, options, fullname, unexpanded_type ) arg = any_type + else: + assert arg is not None args.append(arg) env[tv.id] = arg t.args = tuple(args) @@ -2114,6 +2137,7 @@ def fix_instance( fixed = expand_type(t, env) assert isinstance(fixed, Instance) t.args = fixed.args + return used_default def instantiate_type_alias( @@ -2129,7 +2153,7 @@ def instantiate_type_alias( use_standard_error: bool = False, empty_tuple_index: bool = False, analyzing_tvar_def: bool = False, -) -> Type: +) -> tuple[Type, bool]: """Create an instance of a (generic) type alias from alias node and type arguments. We are following the rules outlined in TypeAlias docstring. @@ -2149,7 +2173,15 @@ def instantiate_type_alias( if any(unknown_unpack(a) for a in args): # This type is not ready to be validated, because of unknown total count. # Note that we keep the kind of Any for consistency. - return set_any_tvars(node, [], ctx.line, ctx.column, options, special_form=True) + return set_any_tvars( + node, + [], + ctx.line, + ctx.column, + options, + special_form=True, + analyzing_tvar_def=analyzing_tvar_def, + ) if ( no_args @@ -2184,8 +2216,8 @@ def instantiate_type_alias( assert isinstance(node.target, Instance) # type: ignore[misc] # Note: this is the only case where we use an eager expansion. See more info about # no_args aliases like L = List in the docstring for TypeAlias class. - return Instance(node.target.type, [], line=ctx.line, column=ctx.column) - return TypeAliasType(node, [], line=ctx.line, column=ctx.column) + return Instance(node.target.type, [], line=ctx.line, column=ctx.column), False + return TypeAliasType(node, [], line=ctx.line, column=ctx.column), False if ( max_tv_count == 0 and act_len > 0 @@ -2197,12 +2229,20 @@ def instantiate_type_alias( tp.column = ctx.column tp.end_line = ctx.end_line tp.end_column = ctx.end_column - return tp + return tp, False if node.tvar_tuple_index is None: if any(isinstance(a, UnpackType) for a in args): # A variadic unpack in fixed size alias (fixed unpacks must be flattened by the caller) fail(message_registry.INVALID_UNPACK_POSITION, ctx, code=codes.VALID_TYPE) - return set_any_tvars(node, [], ctx.line, ctx.column, options, from_error=True) + return set_any_tvars( + node, + [], + ctx.line, + ctx.column, + options, + from_error=True, + analyzing_tvar_def=analyzing_tvar_def, + ) min_tv_count = sum(not tv.has_default() for tv in node.alias_tvars) fill_typevars = act_len != max_tv_count correct = min_tv_count <= act_len <= max_tv_count @@ -2265,7 +2305,15 @@ def instantiate_type_alias( act_suffix = len(args) - unpack - 1 if act_prefix < exp_prefix or act_suffix < exp_suffix: fail("TypeVarTuple cannot be split", ctx, code=codes.TYPE_ARG) - return set_any_tvars(node, [], ctx.line, ctx.column, options, from_error=True) + return set_any_tvars( + node, + [], + ctx.line, + ctx.column, + options, + from_error=True, + analyzing_tvar_def=analyzing_tvar_def, + ) # TODO: we need to check args validity w.r.t alias.alias_tvars. # Otherwise invalid instantiations will be allowed in runtime context. # Note: in type context, these will be still caught by semanal_typeargs. @@ -2278,8 +2326,8 @@ def instantiate_type_alias( ): exp = get_proper_type(typ) assert isinstance(exp, Instance) - return exp.args[-1] - return typ + return exp.args[-1], False + return typ, False def set_any_tvars( @@ -2295,7 +2343,8 @@ def set_any_tvars( fail: MsgCallback | None = None, unexpanded_type: Type | None = None, analyzing_tvar_def: bool = False, -) -> TypeAliasType: +) -> tuple[TypeAliasType, bool]: + used_default = False if from_error or disallow_any: type_of_any = TypeOfAny.from_error elif special_form: @@ -2311,8 +2360,12 @@ def set_any_tvars( if tv is None: continue if arg is None: - if tv.has_default() and not analyzing_tvar_def: + if tv.has_default(): arg = tv.default + if analyzing_tvar_def: + used_default = True + if is_typevar_default_recursive(tv.fullname, node): + arg = any_type else: arg = any_type used_any_type = True @@ -2345,7 +2398,24 @@ def set_any_tvars( Context(newline, newcolumn), code=codes.TYPE_ARG, ) - return t + return t, used_default + + +def is_typevar_default_recursive(tv: str, start: TypeInfo | TypeAlias) -> bool: + if tv not in start.default_depends: + return False + todo = start.default_depends[tv].copy() + seen: set[TypeAlias | TypeInfo] = set() + while todo: + node = todo.pop() + if node is start: + return True + if node in seen: + continue + seen.add(node) + for dep_nodes in node.default_depends.values(): + todo |= dep_nodes + return False class DivergingAliasDetector(TrivialSyntheticTypeTranslator): From dbe1c637cc132a30f5b244ee8acd7a94642cd804 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 May 2026 14:31:14 +0100 Subject: [PATCH 3/8] Add more tests, fix new style classes --- mypy/semanal.py | 2 +- test-data/unit/check-python313.test | 46 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index e51ce599009b2..3a8c12abb9c46 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1992,7 +1992,7 @@ def analyze_class(self, defn: ClassDef) -> None: if any(has_placeholder(tvd) for tvd in tvar_defs): # Some type variable bounds or values are not ready, we need # to re-analyze this class. - self.defer() + self.defer(force_progress=tvar_defs != defn.type_vars) self.analyze_class_keywords(defn) bases_result = self.analyze_base_classes(bases) diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 7976a80b789ac..6557157ea1e55 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -338,6 +338,52 @@ class Bar[U]: def foo(data: U, cond: bool) -> Result[U, str]: return Ok(data) if cond else Err("Error") +[case testRecursiveTypeVarDefaultBasicNewStyle] +class C[T: C = C]: + pass + +c: C +reveal_type(c) # N: Revealed type is "__main__.C[__main__.C[Any]]" + +[case testRecursiveTypeVarDefaultMutualNewStyle] +class C[T = D]: + pass + +class D[S = C]: + pass + +c: C +d: D +reveal_type(c) # N: Revealed type is "__main__.C[__main__.D[Any]]" +reveal_type(d) # N: Revealed type is "__main__.D[__main__.C[Any]]" + +[case testNonRecursiveSimpleTypeVarDefaultNewStyle] +class Child[S = Parent]: ... + +class Parent[T = int]: ... + +reveal_type(Child()) # N: Revealed type is "__main__.Child[__main__.Parent[builtins.int]]" + +[case testRecursiveTypeVarDefaultClassAndAliasNewStyle] +class Trait[T = Pattern]: + pass + +class Pattern1(Trait): + pass +class Pattern2(Trait): + pass + +type Pattern = Pattern1 | Pattern2 + +reveal_type(Trait()) # N: Revealed type is "__main__.Trait[__main__.Pattern1 | __main__.Pattern2]" +[builtins fixtures/tuple.pyi] + +[case testRecursiveTypeVarDefaultOnlyAliasNewStyle] +type A[T = A] = list[T] +a: A +reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" +[builtins fixtures/tuple.pyi] + [case testRecursiveAliasTypeVarDefaultNewStyle] type A[T = A] = int a: A From 8597fc7aa7806ebda35337bf16906fb4eaadb0b7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 May 2026 15:59:31 +0100 Subject: [PATCH 4/8] Some cleanup --- mypy/checkexpr.py | 19 ++++++++--------- mypy/nodes.py | 18 ++++++++++++++-- mypy/semanal.py | 51 ++++++++++++++++++++++++-------------------- mypy/type_visitor.py | 10 ++++++++- mypy/typeanal.py | 17 ++++++++++++--- mypy/types.py | 11 ++++++++-- 6 files changed, 85 insertions(+), 41 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8ecd7eefce251..4c83f5c418f35 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4983,17 +4983,16 @@ class LongName(Generic[T]): ... # A = List[Tuple[T, T]] # x = A() <- same as List[Tuple[Any, Any]], see PEP 484. disallow_any = self.chk.options.disallow_any_generics and self.is_callee - item = get_proper_type( - set_any_tvars( - alias, - [], - ctx.line, - ctx.column, - self.chk.options, - disallow_any=disallow_any, - fail=self.msg.fail, - )[0] + item, _ = set_any_tvars( + alias, + [], + ctx.line, + ctx.column, + self.chk.options, + disallow_any=disallow_any, + fail=self.msg.fail, ) + item = get_proper_type(item) if isinstance(item, Instance): # Normally we get a callable type (or overloaded) with .is_type_obj() true # representing the class's constructor diff --git a/mypy/nodes.py b/mypy/nodes.py index f0216004a45b7..e050a1aa34210 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3168,6 +3168,9 @@ class TypeVarLikeExpr(SymbolNode, Expression): # TypeVar(..., contravariant=True) defines a contravariant type # variable. variance: int + # Record instances and type aliases that appear bare/implicit in the default value + # of this type variable. This is needed to detect recursive type variable defaults. + default_depends: set[TypeInfo | TypeAlias] | None def __init__( self, @@ -3186,7 +3189,7 @@ def __init__( self.default = default self.variance = variance self.is_new_style = is_new_style - self.default_depends: set[TypeInfo | TypeAlias] | None = None + self.default_depends = None @property def name(self) -> str: @@ -3826,6 +3829,16 @@ class is generic then it will be a type constructor of higher kind. # appears in runtime context. type_object_type: mypy.types.FunctionLike | None + # Type variables whose defaults depend on defaults of type variables in other classes + # and type aliases. We keep track of this to safely handle situations like this one: + # class C[T = D]: ... + # class D[S = C]: ... + # x: C + # Since we apply fix_instance() eagerly, inferring a precise type is quite tricky. + # Therefore, we infer the type of `x` as `C[D[Any]]` to avoid infinite recursion. + # Keys are type variable full names. + default_depends: dict[str, set[TypeAlias | TypeInfo]] + FLAGS: Final = [ "is_abstract", "is_enum", @@ -3887,7 +3900,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None self.is_type_check_only = False self.deprecated = None self.type_object_type = None - self.default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} + self.default_depends = {} def add_type_vars(self) -> None: self.has_type_var_tuple_type = False @@ -4586,6 +4599,7 @@ def __init__( self.eager = eager self.python_3_12_type_alias = python_3_12_type_alias self.tvar_tuple_index = None + # This plays the same role as TypeInfo.default_depends attribute. self.default_depends: dict[str, set[TypeAlias | TypeInfo]] = {} for i, t in enumerate(alias_tvars): if isinstance(t, mypy.types.TypeVarTupleType): diff --git a/mypy/semanal.py b/mypy/semanal.py index 3a8c12abb9c46..7775b798c95f4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -538,6 +538,10 @@ def __init__( # [b.py] # import foo.bar self.transitive_submodule_imports: dict[str, set[str]] = {} + + # Instances and type aliases that were fixed using default valuers of type + # variables. This can be used on-demand by type analyzer. Use record_fixed_type() + # to create the set lazily. self.types_fixed: set[TypeInfo | TypeAlias] | None = None # mypyc doesn't properly handle implementing an abstractproperty @@ -1990,8 +1994,10 @@ def analyze_class(self, defn: ClassDef) -> None: default_depends[tv.fullname] = tv.default_depends if any(has_placeholder(tvd) for tvd in tvar_defs): - # Some type variable bounds or values are not ready, we need - # to re-analyze this class. + # Some type variable bounds or values are not ready, we need to + # re-analyze this class. Note we force progress to handle cases like + # class C[T = C], this matches logic in process_typevar_parameters() + # for "old style" type variables. self.defer(force_progress=tvar_defs != defn.type_vars) self.analyze_class_keywords(defn) @@ -2283,7 +2289,8 @@ class Foo(Bar, Generic[T]): ... Note that this is performed *before* semantic analysis. - Returns (remaining base expressions, inferred type variables, is protocol). + Returns a tuple: + (remaining base expressions, type variables, is protocol, type variable expressions). """ removed: list[int] = [] declared_tvars: TypeVarLikeList = [] @@ -3993,7 +4000,8 @@ def analyze_alias( """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type, a list of type variables for generic aliases, - a set of names the alias depends on, and True if the original type has empty tuple index. + a set of names the alias depends on, whether the original type has empty tuple index, + and any type variables whose defaults depend on other classes or type aliases. An example for the dependencies: A = int B = str @@ -4179,9 +4187,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: self.mark_incomplete(lvalue.name, rvalue, becomes_typeinfo=True) return True - if any(has_placeholder(tv) for tv in alias_tvars): - self.defer() - self.add_type_alias_deps(depends_on) check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg, context=s) # When this type alias gets "inlined", the Any is not explicit anymore, @@ -4236,7 +4241,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # An alias gets updated. updated = False if isinstance(existing.node, TypeAlias): - if existing.node.target != res or existing.node.alias_tvars != alias_tvars: + if existing.node.target != res: # Copy expansion to the existing alias, this matches how we update base classes # for a TypeInfo _in place_ if there are nested placeholders. existing.node.target = res @@ -4250,6 +4255,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # Otherwise just replace existing placeholder with type alias *in place*. existing._node = alias_node updated = True + # TODO: switch type aliases to if has_placeholder(): process_placeholder() pattern. + # Type aliases are last notable exception from this logic. if updated: if self.final_iteration: self.cannot_resolve_name(lvalue.name, "name", s) @@ -4773,6 +4780,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values = call.arg_kinds[1:].count(ARG_POS) values = self.analyze_value_types(call.args[1 : 1 + n_values]) + # Reset fixed types both before and after each collection just in case. self.types_fixed = None res = self.process_typevar_parameters( call.args[1 + n_values :], @@ -5027,18 +5035,16 @@ def get_typevarlike_argument( # class Custom(Generic[T]): # ... analyzed = PlaceholderType(None, [], context.line) - if report_invalid_typevar_arg: - if ( - isinstance(analyzed, ProperType) - and isinstance(analyzed, AnyType) - and analyzed.is_from_error - ): - self.fail( - message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format( - typevarlike_name, param_name - ), - param_value, - ) + if ( + report_invalid_typevar_arg + and isinstance(analyzed, ProperType) + and isinstance(analyzed, AnyType) + and analyzed.is_from_error + ): + self.fail( + message_registry.TYPEVAR_ARG_MUST_BE_TYPE.format(typevarlike_name, param_name), + param_value, + ) # Note: we do not return 'None' here -- we want to continue # using the AnyType. return analyzed @@ -5757,10 +5763,9 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: self.mark_incomplete(s.name.name, s.value, becomes_typeinfo=True) return - # Now go through all new variables and temporary replace all tvars that still - # refer to some placeholders. We defer the whole alias and will revisit it again, - # as well as all its dependents. if any(has_placeholder(tv) for tv in alias_tvars): + # Defer the alias if some type variables are not ready, same as for classes. + # Note: progress is forced below (if needed). self.defer() self.add_type_alias_deps(depends_on) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 58118f2554cb3..d668121bc5b9c 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -595,7 +595,15 @@ def visit_type_alias_type(self, t: TypeAliasType, /) -> bool: elif t in self.seen_aliases: return self.default self.seen_aliases.add(t) - return get_proper_type(t).accept(self) or self.query_types(t.args) + res = get_proper_type(t).accept(self) + # This is a weird edge case: if a type alias has unused type variables, we + # should visit arguments even if we didn't find anything in the expansion. + # As an optimization, do this only for new style type aliases. + assert t.alias is not None + if self.strategy == ANY_STRATEGY: + return res or (t.alias.python_3_12_type_alias and self.query_types(t.args)) + else: + return res and (not t.alias.python_3_12_type_alias or self.query_types(t.args)) def query_types(self, types: list[Type] | tuple[Type, ...]) -> bool: """Perform a query for a sequence of types using the strategy to combine the results.""" diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 95e096b3d312e..de5adbf3dbb86 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -269,6 +269,7 @@ def __init__( self.allow_type_any = allow_type_any self.allow_type_var_tuple = False self.allow_unpack = allow_unpack + # Set when we are analyzing a default of a type variable. self.analyzing_tvar_def = analyzing_tvar_def def lookup_qualified( @@ -914,6 +915,8 @@ def analyze_type_with_type_info( analyzing_tvar_def=self.analyzing_tvar_def, ) if self.analyzing_tvar_def and used_default: + # For convenience, we make default depend on the original TypeInfo, + # *not* on the special alias. self.api.record_fixed_type(info) return res return tup.copy_modified( @@ -936,6 +939,8 @@ def analyze_type_with_type_info( analyzing_tvar_def=self.analyzing_tvar_def, ) if self.analyzing_tvar_def and used_default: + # For convenience, we make default depend on the original TypeInfo, + # *not* on the special alias. self.api.record_fixed_type(info) return res # Create a named TypedDictType @@ -2114,8 +2119,10 @@ def fix_instance( if tv.has_default(): arg = tv.default if analyzing_tvar_def: + # Record the use of default only when analyzing another default. used_default = True if is_typevar_default_recursive(tv.fullname, t.type): + # If this results in infinite recursion, use Any instead. use_any = True else: use_any = True @@ -2362,6 +2369,7 @@ def set_any_tvars( if arg is None: if tv.has_default(): arg = tv.default + # Same as for instances, record and avoid infinite recursion. if analyzing_tvar_def: used_default = True if is_typevar_default_recursive(tv.fullname, node): @@ -2401,16 +2409,19 @@ def set_any_tvars( return t, used_default -def is_typevar_default_recursive(tv: str, start: TypeInfo | TypeAlias) -> bool: - if tv not in start.default_depends: +def is_typevar_default_recursive(tv_fname: str, start: TypeInfo | TypeAlias) -> bool: + """Check if the type variable can lead to infinite recursion via defaults.""" + if tv_fname not in start.default_depends: return False - todo = start.default_depends[tv].copy() + todo = start.default_depends[tv_fname].copy() seen: set[TypeAlias | TypeInfo] = set() while todo: node = todo.pop() if node is start: return True if node in seen: + # We don't return True here, since we are interested only in + # recursion via the original type variable. continue seen.add(node) for dep_nodes in node.default_depends.values(): diff --git a/mypy/types.py b/mypy/types.py index 8baa0044de890..d0b0f1b7a1bc2 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -855,7 +855,12 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, ParamSpecType): return NotImplemented # Upper bound can be ignored, since it's determined by flavor. - return self.id == other.id and self.flavor == other.flavor and self.prefix == other.prefix + return ( + self.id == other.id + and self.flavor == other.flavor + and self.prefix == other.prefix + and self.default == other.default + ) def serialize(self) -> JsonDict: assert not self.id.is_meta_var() @@ -1004,7 +1009,9 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: if not isinstance(other, TypeVarTupleType): return NotImplemented - return self.id == other.id and self.min_len == other.min_len + return ( + self.id == other.id and self.min_len == other.min_len and self.default == other.default + ) def copy_modified( self, From 43586b637a6442026abe8aaa7c26d0f114729dd7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 May 2026 16:27:43 +0100 Subject: [PATCH 5/8] Fix self-compilation --- mypy/semanal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 7775b798c95f4..dca46a729bcee 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1910,7 +1910,7 @@ def analyze_type_param( default = self.check_typevartuple_default(default, type_param.default) else: default = AnyType(TypeOfAny.from_omitted_generics) - default_depends = self.types_fixed + default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed self.types_fixed = None if type_param.kind == TYPE_VAR_KIND: values: list[Type] = [] @@ -4789,7 +4789,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values, s, ) - default_depends = self.types_fixed + default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed self.types_fixed = None if res is None: return False @@ -5117,7 +5117,7 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: "The variance and bound arguments to ParamSpec do not have defined semantics yet", s, ) - default_depends = self.types_fixed + default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed self.types_fixed = None # PEP 612 reserves the right to define bound, covariant and contravariant arguments to @@ -5178,7 +5178,7 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: else: self.fail(f'Unexpected keyword argument "{param_name}" for "TypeVarTuple"', s) - default_depends = self.types_fixed + default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed self.types_fixed = None name = self.extract_typevarlike_name(s, call) From e456cbef30b66d93285e07a19726387e71560f0e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 May 2026 16:39:48 +0100 Subject: [PATCH 6/8] Try better self-compilation fix --- mypy/semanal.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index dca46a729bcee..2e66ba4e65aef 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1888,7 +1888,9 @@ def analyze_type_param( upper_bound = self.named_type("builtins.tuple", [self.object_type()]) else: upper_bound = self.object_type() - self.types_fixed = None + # Reset fixed types both before and after each collection just in case. + if self.types_fixed is not None: + self.types_fixed.clear() if type_param.default: default = self.anal_type( type_param.default, @@ -1910,7 +1912,7 @@ def analyze_type_param( default = self.check_typevartuple_default(default, type_param.default) else: default = AnyType(TypeOfAny.from_omitted_generics) - default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed + default_depends = self.types_fixed self.types_fixed = None if type_param.kind == TYPE_VAR_KIND: values: list[Type] = [] @@ -4780,8 +4782,8 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values = call.arg_kinds[1:].count(ARG_POS) values = self.analyze_value_types(call.args[1 : 1 + n_values]) - # Reset fixed types both before and after each collection just in case. - self.types_fixed = None + if self.types_fixed is not None: + self.types_fixed.clear() res = self.process_typevar_parameters( call.args[1 + n_values :], call.arg_names[1 + n_values :], @@ -4789,7 +4791,7 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool: n_values, s, ) - default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed + default_depends = self.types_fixed self.types_fixed = None if res is None: return False @@ -5092,7 +5094,8 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: if n_values != 0: self.fail('Too many positional arguments for "ParamSpec"', s) - self.types_fixed = None + if self.types_fixed is not None: + self.types_fixed.clear() default: Type = AnyType(TypeOfAny.from_omitted_generics) for param_value, param_name in zip( call.args[1 + n_values :], call.arg_names[1 + n_values :] @@ -5117,7 +5120,7 @@ def process_paramspec_declaration(self, s: AssignmentStmt) -> bool: "The variance and bound arguments to ParamSpec do not have defined semantics yet", s, ) - default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed + default_depends = self.types_fixed self.types_fixed = None # PEP 612 reserves the right to define bound, covariant and contravariant arguments to @@ -5159,7 +5162,8 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: self.fail('Too many positional arguments for "TypeVarTuple"', s) default: Type = AnyType(TypeOfAny.from_omitted_generics) - self.types_fixed = None + if self.types_fixed is not None: + self.types_fixed.clear() for param_value, param_name in zip( call.args[1 + n_values :], call.arg_names[1 + n_values :] ): @@ -5178,7 +5182,7 @@ def process_typevartuple_declaration(self, s: AssignmentStmt) -> bool: else: self.fail(f'Unexpected keyword argument "{param_name}" for "TypeVarTuple"', s) - default_depends: set[TypeInfo | TypeAlias] | None = self.types_fixed + default_depends = self.types_fixed self.types_fixed = None name = self.extract_typevarlike_name(s, call) From 0f47216219eedf5d7ecb2d1633a9005faab1ac7c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 May 2026 19:15:08 +0100 Subject: [PATCH 7/8] Fix base classes --- mypy/semanal.py | 4 +++ test-data/unit/check-python313.test | 35 +++++++++++++++++++ test-data/unit/check-typevar-defaults.test | 39 ++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index 2e66ba4e65aef..2666aa7fbf6af 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2015,6 +2015,10 @@ def analyze_class(self, defn: ClassDef) -> None: # are okay in nested positions, since they can't affect the MRO. self.mark_incomplete(defn.name, defn) return + if any(has_placeholder(base) for base, _ in base_types): + # We need to manually call defer() in case a placeholder was brought by a + # type variable default, so that type analyzer didn't call it. + self.defer() declared_metaclass, should_defer, any_meta = self.get_declared_metaclass( defn.name, defn.metaclass diff --git a/test-data/unit/check-python313.test b/test-data/unit/check-python313.test index 6557157ea1e55..87bd722742b50 100644 --- a/test-data/unit/check-python313.test +++ b/test-data/unit/check-python313.test @@ -364,6 +364,41 @@ class Parent[T = int]: ... reveal_type(Child()) # N: Revealed type is "__main__.Child[__main__.Parent[builtins.int]]" +[case testNonRecursiveTypeVarDefaultImportCycleClassNewStyle] +import exp +[file exp.pyi] +import ind + +class F[T: D = D]: + x: T + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +class Ind(F): ... +x: Ind +reveal_type(x.x) # N: Revealed type is "exp.D" + +[case testNonRecursiveTypeVarDefaultImportCycleAliasNewStyle] +import exp +[file exp.pyi] +import ind + +type F[T: D = D] = list[T] + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +type Ind = list[F] +x: Ind +reveal_type(x) # N: Revealed type is "builtins.list[builtins.list[exp.D]]" + [case testRecursiveTypeVarDefaultClassAndAliasNewStyle] class Trait[T = Pattern]: pass diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 7463d0ad05270..5895830631b4a 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -977,6 +977,45 @@ class Parent(Generic[T]): ... reveal_type(Child()) # N: Revealed type is "__main__.Child[__main__.Parent[builtins.int]]" +[case testNonRecursiveTypeVarDefaultImportCycleClass] +import exp +[file exp.pyi] +from typing import Generic, TypeVar +import ind + +T = TypeVar("T", bound=D, default=D) +class F(Generic[T]): + x: T + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +class Ind(F): ... +x: Ind +reveal_type(x.x) # N: Revealed type is "exp.D" + +[case testNonRecursiveTypeVarDefaultImportCycleAlias] +import exp +[file exp.pyi] +from typing import TypeVar +import ind + +T = TypeVar("T", bound=D, default=D) +F = list[T] + +class D(E): ... +class E: ... + +[file ind.pyi] +from exp import F + +Ind = list[F] +x: Ind +reveal_type(x) # N: Revealed type is "builtins.list[builtins.list[exp.D]]" + [case testRecursiveTypeVarDefaultClassAndAlias] from typing import Generic, TypeVar, Union From c0e7eda9e676df89f993de7476c0922a174ee92a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 May 2026 22:01:45 +0100 Subject: [PATCH 8/8] Add custom note in case Any was forced --- mypy/checkexpr.py | 1 + mypy/message_registry.py | 1 + mypy/typeanal.py | 63 +++++++++++++++++---------------- test-data/unit/check-flags.test | 31 ++++++++++++++++ 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4c83f5c418f35..f93982a12a72d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4919,6 +4919,7 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: tapp.expr.node, tapp.types, self.chk.fail, + self.chk.note, tapp.expr.node.no_args, tapp, self.chk.options, diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 30ced27aef22f..82885065934f1 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -180,6 +180,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage: IMPLICIT_GENERIC_ANY_BUILTIN: Final = ( 'Implicit generic "Any". Use "{}" and specify generic parameters' ) +NO_CYCLIC_DEFAULT: Final = "Cyclic type variable defaults are not supported" INVALID_UNPACK: Final = "{} cannot be unpacked (must be tuple or TypeVarTuple)" INVALID_UNPACK_POSITION: Final = "Unpack is only valid in a variadic position" INVALID_PARAM_SPEC_LOCATION: Final = "Invalid location for ParamSpec {}" diff --git a/mypy/typeanal.py b/mypy/typeanal.py index de5adbf3dbb86..c5101fd8ee217 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -480,6 +480,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) node, an_args, self.fail, + self.note, node.no_args, t, self.options, @@ -908,6 +909,7 @@ def analyze_type_with_type_info( # TODO: should we allow NamedTuples generic in ParamSpec? self.anal_array(args, allow_unpack=True), self.fail, + self.note, False, ctx, self.options, @@ -932,6 +934,7 @@ def analyze_type_with_type_info( # TODO: should we allow TypedDicts generic in ParamSpec? self.anal_array(args, allow_unpack=True), self.fail, + self.note, False, ctx, self.options, @@ -2051,6 +2054,7 @@ def get_omitted_any( options: Options, fullname: str | None = None, unexpanded_type: Type | None = None, + used_default: bool = False, ) -> AnyType: if disallow_any: typ = unexpanded_type or orig_type @@ -2061,6 +2065,8 @@ def get_omitted_any( typ, code=codes.TYPE_ARG, ) + if used_default: + note(message_registry.NO_CYCLIC_DEFAULT, typ, code=codes.TYPE_ARG) any_type = AnyType(TypeOfAny.from_error, line=typ.line, column=typ.column) else: @@ -2130,7 +2136,14 @@ def fix_instance( if any_type is None: fullname = None if use_generic_error else t.type.fullname any_type = get_omitted_any( - disallow_any, fail, note, t, options, fullname, unexpanded_type + disallow_any, + fail, + note, + t, + options, + fullname, + unexpanded_type, + used_default, ) arg = any_type else: @@ -2151,6 +2164,7 @@ def instantiate_type_alias( node: TypeAlias, args: list[Type], fail: MsgCallback, + note: MsgCallback, no_args: bool, ctx: Context, options: Options, @@ -2180,15 +2194,7 @@ def instantiate_type_alias( if any(unknown_unpack(a) for a in args): # This type is not ready to be validated, because of unknown total count. # Note that we keep the kind of Any for consistency. - return set_any_tvars( - node, - [], - ctx.line, - ctx.column, - options, - special_form=True, - analyzing_tvar_def=analyzing_tvar_def, - ) + return set_any_tvars(node, [], ctx.line, ctx.column, options, special_form=True) if ( no_args @@ -2215,6 +2221,7 @@ def instantiate_type_alias( options, disallow_any=disallow_any, fail=fail, + note=note, unexpanded_type=unexpanded_type, analyzing_tvar_def=analyzing_tvar_def, ) @@ -2241,15 +2248,7 @@ def instantiate_type_alias( if any(isinstance(a, UnpackType) for a in args): # A variadic unpack in fixed size alias (fixed unpacks must be flattened by the caller) fail(message_registry.INVALID_UNPACK_POSITION, ctx, code=codes.VALID_TYPE) - return set_any_tvars( - node, - [], - ctx.line, - ctx.column, - options, - from_error=True, - analyzing_tvar_def=analyzing_tvar_def, - ) + return set_any_tvars(node, [], ctx.line, ctx.column, options, from_error=True) min_tv_count = sum(not tv.has_default() for tv in node.alias_tvars) fill_typevars = act_len != max_tv_count correct = min_tv_count <= act_len <= max_tv_count @@ -2296,7 +2295,10 @@ def instantiate_type_alias( ctx.line, ctx.column, options, - from_error=True, + disallow_any=disallow_any, + fail=fail, + note=note, + from_error=not correct, analyzing_tvar_def=analyzing_tvar_def, ) elif node.tvar_tuple_index is not None: @@ -2312,15 +2314,7 @@ def instantiate_type_alias( act_suffix = len(args) - unpack - 1 if act_prefix < exp_prefix or act_suffix < exp_suffix: fail("TypeVarTuple cannot be split", ctx, code=codes.TYPE_ARG) - return set_any_tvars( - node, - [], - ctx.line, - ctx.column, - options, - from_error=True, - analyzing_tvar_def=analyzing_tvar_def, - ) + return set_any_tvars(node, [], ctx.line, ctx.column, options, from_error=True) # TODO: we need to check args validity w.r.t alias.alias_tvars. # Otherwise invalid instantiations will be allowed in runtime context. # Note: in type context, these will be still caught by semanal_typeargs. @@ -2348,6 +2342,7 @@ def set_any_tvars( disallow_any: bool = False, special_form: bool = False, fail: MsgCallback | None = None, + note: MsgCallback | None = None, unexpanded_type: Type | None = None, analyzing_tvar_def: bool = False, ) -> tuple[TypeAliasType, bool]: @@ -2374,6 +2369,7 @@ def set_any_tvars( used_default = True if is_typevar_default_recursive(tv.fullname, node): arg = any_type + used_any_type = True else: arg = any_type used_any_type = True @@ -2390,7 +2386,7 @@ def set_any_tvars( assert isinstance(fixed, TypeAliasType) t.args = fixed.args - if used_any_type and disallow_any and node.alias_tvars: + if used_any_type and disallow_any and node.alias_tvars and not from_error: assert fail is not None if unexpanded_type: type_str = ( @@ -2406,6 +2402,13 @@ def set_any_tvars( Context(newline, newcolumn), code=codes.TYPE_ARG, ) + if used_default: + assert note is not None + note( + message_registry.NO_CYCLIC_DEFAULT, + Context(newline, newcolumn), + code=codes.TYPE_ARG, + ) return t, used_default diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index a281218af58e0..dd4687181ca41 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2717,3 +2717,34 @@ if ( or z is None ): pass + +[case testRecursiveTypeVarDefaultMutualDisallow] +# flags: --disallow-any-generic +from typing import TypeVar, Generic + +class C(Generic["T"]): + pass + +class D(Generic["S"]): + pass + +T = TypeVar("T", default=D) # E: Missing type arguments for generic type "D" \ + # N: Cyclic type variable defaults are not supported +S = TypeVar("S", default=C) # E: Missing type arguments for generic type "C" \ + # N: Cyclic type variable defaults are not supported + +c: C +d: D +reveal_type(c) # N: Revealed type is "__main__.C[__main__.D[Any]]" +reveal_type(d) # N: Revealed type is "__main__.D[__main__.C[Any]]" + +[case testRecursiveTypeVarDefaultOnlyAliasDisallow] +# flags: --disallow-any-generic +from typing import TypeVar + +T = TypeVar("T", default="A") # E: Missing type arguments for generic type "A" \ + # N: Cyclic type variable defaults are not supported +A = list[T] +a: A +reveal_type(a) # N: Revealed type is "builtins.list[builtins.list[Any]]" +[builtins fixtures/tuple.pyi]