From 552c4007c2cb7f28315f882cb21ab3b99702a09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Furga=C5=82a?= <83299832+00200200@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:42:43 +0200 Subject: [PATCH 1/2] Allow Any-typed tuple indexes as dynamic class bases. Mypy already accepts a name of type Any as a base class, but treated args[1] as a generic type application, so *args entries typed as Any were rejected. Index into a known tuple type in base-class position and reuse the existing Any/type/Type[Any] rule without relaxing non-Any bases. --- mypy/typeanal.py | 62 ++++++++++++++++++++++++---- test-data/unit/check-classes.test | 68 +++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8d500c54364a..276f5a4292ff 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1007,17 +1007,19 @@ def analyze_unbound_type_without_type_info( # context. This is slightly problematic as it allows using the type 'Any' # as a base class -- however, this will fail soon at runtime so the problem # is pretty minor. + # + # Also allow indexing a tuple-typed value that yields Any (or type / Type[Any]) + # in base class position, e.g. class C(args[1]) where args: tuple[int, Any]. if isinstance(sym.node, Var): typ = get_proper_type(sym.node.type) - if isinstance(typ, AnyType): - return AnyType( - TypeOfAny.from_unimported_type, missing_import_name=typ.missing_import_name - ) - elif self.allow_type_any: - if isinstance(typ, Instance) and typ.type.fullname == "builtins.type": - return AnyType(TypeOfAny.special_form) - if isinstance(typ, TypeType) and isinstance(typ.item, AnyType): - return AnyType(TypeOfAny.from_another_any, source_any=typ.item) + runtime_type = self.anal_type_from_runtime_value(typ) + if runtime_type is not None: + return runtime_type + if self.allow_type_any and t.args: + item = self.indexed_tuple_item_type(typ, t.args) + runtime_type = self.anal_type_from_runtime_value(item) + if runtime_type is not None: + return runtime_type # Option 2: # Unbound type variable. Currently these may be still valid, # for example when defining a generic type alias. @@ -1121,6 +1123,48 @@ def analyze_unbound_type_without_type_info( # see https://github.com/python/mypy/issues/4987. return t + def anal_type_from_runtime_value(self, typ: ProperType | None) -> Type | None: + """Return a type if a runtime value of this type may be used as a type. + + This is used for dynamic base classes: a variable of type Any, type, or + Type[Any] is a valid base. Returns None if the value is not valid as a type. + """ + if isinstance(typ, AnyType): + return AnyType( + TypeOfAny.from_unimported_type, missing_import_name=typ.missing_import_name + ) + if self.allow_type_any: + if isinstance(typ, Instance) and typ.type.fullname == "builtins.type": + return AnyType(TypeOfAny.special_form) + if isinstance(typ, TypeType) and isinstance(typ.item, AnyType): + return AnyType(TypeOfAny.from_another_any, source_any=typ.item) + return None + + def indexed_tuple_item_type( + self, typ: ProperType | None, args: tuple[Type, ...] + ) -> ProperType | None: + """If this looks like indexing a tuple-typed value, return the item type.""" + if typ is None or len(args) != 1: + return None + if isinstance(typ, Instance) and typ.type.fullname in TUPLE_NAMES and typ.args: + # Homogeneous tuple[T, ...]: any index has type T. + return get_proper_type(typ.args[0]) + if isinstance(typ, TupleType): + if find_unpack_in_list(typ.items) is not None: + return None + index = args[0] + if not isinstance(index, RawExpressionType) or not isinstance( + index.literal_value, int + ): + return None + i = index.literal_value + n = len(typ.items) + if i < 0: + i += n + if 0 <= i < n: + return get_proper_type(typ.items[i]) + return None + def visit_any(self, t: AnyType) -> Type: return t diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index dc5d74abc07d..fc0671a2962f 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9727,3 +9727,71 @@ def f() -> None: class X: ... undefined # E: Name "undefined" is not defined + +[case testSubclassAnyFromIndexedTuple] +# https://github.com/python/mypy/issues/21998 +from typing import Any, Tuple + +def from_tuple(t: Tuple[int, Any]) -> None: + class Sub(t[1]): + pass + +def from_star_any(*args: Any) -> None: + class Sub(args[0]): + pass + +def from_any_param(base: Any) -> None: + class Sub(base): + pass + +def from_annotated_index(*args: Any) -> None: + x: Any = args[0] + class Sub(x): + pass +[builtins fixtures/tuple.pyi] + +[case testSubclassAnyFromUnpackedStarArgs] +# https://github.com/python/mypy/issues/21998 +# flags: --python-version 3.12 +from typing import Any + +def from_unpacked(*args: *tuple[int, Any]) -> None: + class Sub(args[1]): + pass + +def from_unpacked_type_annotation(*args: *tuple[int, Any]) -> None: + x: type = args[1] + class Sub(x): + pass +[builtins fixtures/tuple.pyi] + +[case testSubclassIndexedNonAnyStillInvalid] +# https://github.com/python/mypy/issues/21998 +from typing import Any, Tuple + +def from_str_tuple(t: Tuple[int, str]) -> None: + class Sub(t[1]): pass # E: Variable "t" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "t" \ + # E: Invalid type: try using Literal[1] instead? + +def from_int_index(t: Tuple[int, Any]) -> None: + class Sub(t[0]): pass # E: Variable "t" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "t" \ + # E: Invalid type: try using Literal[0] instead? + +def from_int_star(*args: int) -> None: + class Sub(args[0]): pass # E: Variable "args" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "args" \ + # E: Invalid type: try using Literal[0] instead? + +def unannotated_local(base: Any) -> None: + # Class bases are resolved before type inference, so an unannotated + # local still needs an explicit Any/type annotation. + x = base + class Sub(x): pass # E: Variable "x" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "x" +[builtins fixtures/tuple.pyi] From e0228a2d70f711361b48041cf5f6b248e07c1e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Furga=C5=82a?= <83299832+00200200@users.noreply.github.com> Date: Fri, 18 Sep 2026 08:09:20 +0200 Subject: [PATCH 2/2] Allow Any from nested unpacked tuple indexes as class bases. Flatten Unpack[Tuple[...]] before reading a literal index so *tuple[int, *tuple[Any]] is treated like tuple[int, Any]. --- mypy/typeanal.py | 7 ++++--- test-data/unit/check-classes.test | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 276f5a4292ff..f64164c14905 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1150,7 +1150,8 @@ def indexed_tuple_item_type( # Homogeneous tuple[T, ...]: any index has type T. return get_proper_type(typ.args[0]) if isinstance(typ, TupleType): - if find_unpack_in_list(typ.items) is not None: + items = flatten_nested_tuples(typ.items) + if find_unpack_in_list(items) is not None: return None index = args[0] if not isinstance(index, RawExpressionType) or not isinstance( @@ -1158,11 +1159,11 @@ def indexed_tuple_item_type( ): return None i = index.literal_value - n = len(typ.items) + n = len(items) if i < 0: i += n if 0 <= i < n: - return get_proper_type(typ.items[i]) + return get_proper_type(items[i]) return None def visit_any(self, t: AnyType) -> Type: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index fc0671a2962f..98771ec77790 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9765,6 +9765,23 @@ def from_unpacked_type_annotation(*args: *tuple[int, Any]) -> None: pass [builtins fixtures/tuple.pyi] +[case testSubclassAnyFromNestedUnpackedStarArgs] +# https://github.com/python/mypy/issues/21998 +# flags: --python-version 3.12 +from typing import Any + +def from_unpacked(*args: *tuple[int, *tuple[Any]]) -> None: + reveal_type(args) # N: Revealed type is "tuple[builtins.int, Any]" + class Sub(args[1]): + pass + +def from_unpacked_type_annotation(*args: *tuple[int, *tuple[Any]]) -> None: + reveal_type(args) # N: Revealed type is "tuple[builtins.int, Any]" + x: type = args[1] + class Sub(x): + pass +[builtins fixtures/tuple.pyi] + [case testSubclassIndexedNonAnyStillInvalid] # https://github.com/python/mypy/issues/21998 from typing import Any, Tuple