Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c56f103
Accept inferred explicit-Any variable as base class
Dextheking1 Sep 24, 2026
55d210e
Merge remote-tracking branch 'upstream/master' into fix/21998-inferre…
Dextheking1 Sep 24, 2026
0335237
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 24, 2026
868b541
Restore mypy core modules deleted by bad push
Dextheking1 Sep 24, 2026
08ce848
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 24, 2026
4e5a044
Restore #21998 implementation lost in bad merge
Dextheking1 Sep 24, 2026
e7381ca
Fix self-check errors in check_deferred_base_classes
Dextheking1 Sep 24, 2026
5b2acbc
test: add tuple-index Any base class cases from #22002
Dextheking1 Sep 28, 2026
a5d92f3
test: pin rejection of Any leaking from unannotated code
Dextheking1 Sep 28, 2026
febb05a
Do not report deferred base class errors where semanal would not (1/5…
Dextheking1 Sep 28, 2026
28683a3
Do not report deferred base class errors where semanal would not (2/5…
Dextheking1 Sep 28, 2026
aafedde
Do not report deferred base class errors where semanal would not (3/5…
Dextheking1 Sep 28, 2026
7337b49
Do not report deferred base class errors where semanal would not (4/5…
Dextheking1 Sep 28, 2026
6e20719
Do not report deferred base class errors where semanal would not (5/5…
Dextheking1 Sep 28, 2026
1a41018
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 28, 2026
17fab6c
Address review nits: use dataclass for deferred bases, simplify is_ex…
Dextheking1 Oct 7, 2026
4eb0004
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 7, 2026
da7c5d1
Merge master into fix/21998-inferred-any-base-class
Dextheking1 Oct 23, 2026
9024726
Support subscripted variable bases with Any item type
Dextheking1 Oct 23, 2026
4e6dc32
Support type | Any deferred base classes, fold index expressions gene…
Dextheking1 Oct 25, 2026
4b224fd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 25, 2026
60a8ea5
add reveal_type to before/after unpack cases
Dextheking1 Oct 25, 2026
4297367
Do not defer variables nested in base class type arguments
Dextheking1 Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def __init__(self) -> None:
is_instance_var,
)
from mypy.checkpattern import PatternChecker
from mypy.constant_fold import constant_fold_expr
from mypy.constraints import SUPERTYPE_OF
from mypy.erasetype import (
erase_type,
Expand Down Expand Up @@ -207,6 +208,7 @@ def __init__(self) -> None:
WithStmt,
YieldExpr,
get_func_def,
get_member_expr_fullname,
is_final_node,
)
from mypy.operators import flip_ops, int_op_to_method, neg_ops
Expand All @@ -220,6 +222,7 @@ def __init__(self) -> None:
from mypy.semanal_shared import SemanticAnalyzerCoreInterface
from mypy.sharedparse import BINARY_MAGIC_METHODS
from mypy.state import state
from mypy.stats import get_original_any
from mypy.subtypes import (
find_member,
has_any_type,
Expand Down Expand Up @@ -266,6 +269,7 @@ def __init__(self) -> None:
MYPYC_NATIVE_INT_NAMES,
NOT_IMPLEMENTED_TYPE_NAMES,
OVERLOAD_NAMES,
TUPLE_NAMES,
AnyType,
BoolTypeQuery,
CallableType,
Expand Down Expand Up @@ -296,6 +300,7 @@ def __init__(self) -> None:
UnpackType,
extend_args_for_prefix_and_suffix,
find_unpack_in_list,
flatten_nested_tuples,
flatten_nested_unions,
get_proper_type,
get_proper_types,
Expand Down Expand Up @@ -2839,9 +2844,218 @@ def check__exit__return_type(self, defn: FuncItem) -> None:
):
self.msg.incorrect__exit__return(defn)

def is_explicit_any(self, typ: ProperType) -> bool:
"""Is this an Any type that comes from an explicit annotation?

Unlike ``Any`` that leaks in from unannotated (dynamically typed) code,
an explicitly written ``Any`` means the user opted into dynamic typing,
so it is treated like an explicitly ``Any``-typed variable.
"""
if not isinstance(typ, AnyType):
return False
# Unwrap Any types that come from another Any to get the original.
typ = get_original_any(typ)
# Only accept explicitly written Any, not Any from unannotated code
# or other sources.
return typ.type_of_any == TypeOfAny.explicit

def check_deferred_base_classes(self, defn: ClassDef) -> None:
"""Validate base classes deferred from semantic analysis.

A base class that is a variable with an inferred (not declared) type --
e.g. ``x = args[1]`` where ``args[1]`` is ``Any`` -- could not be
validated during semantic analysis, since the type is only inferred by
the type checker. Such bases were provisionally treated as ``Any``;
now that the type is known, accept it if it is ``Any`` (like an
explicitly ``Any``-typed variable) and otherwise report the errors that
semantic analysis deferred.

A base class may also subscript a variable directly, e.g.
``class Sub(args[1])``. Then the item type at the subscript is what
gets validated, in the same way.
"""
if not self.in_checked_function():
# Semantic analysis silently drops these errors in unchecked
# functions; do the same here to avoid new reports.
return
for deferred in defn.info.deferred_base_classes:
var = deferred.var
base_expr = deferred.expr
defining_literal = deferred.defining_literal
if defining_literal:
# Semantic analysis does not report "not valid as a type" for
# variables inside Literal[...]; do the same here.
continue
if isinstance(base_expr, IndexExpr):
typ = self.deferred_index_base_type(var, base_expr)
else:
var_type = var.type
typ = get_proper_type(var_type) if var_type is not None else None
# Mirror the cases semantic analysis accepts for an explicitly
# typed variable (see TypeAnalyser.analyze_unbound_type_without_type_info).
# An inferred plain ``Any`` is only accepted if it comes from an
# explicit annotation; ``Any`` leaking in from unannotated code is
# still rejected, as before.
is_any = typ is not None and self.is_valid_deferred_base_type(typ)
if is_any:
if self.options.disallow_subclassing_any:
if isinstance(base_expr, (NameExpr, MemberExpr)):
msg = f'Class cannot subclass "{base_expr.name}" (has type "Any")'
else:
msg = 'Class cannot subclass value of type "Any"'
self.fail(msg, base_expr)
continue
if isinstance(base_expr, NameExpr):
name_repr: str | None = base_expr.name
elif isinstance(base_expr, MemberExpr):
name_repr = get_member_expr_fullname(base_expr)
elif isinstance(base_expr, IndexExpr):
name_repr = var.name
else:
name_repr = None
self.fail(
f'Variable "{var.fullname}" is not valid as a type',
base_expr,
code=codes.VALID_TYPE,
)
self.note(
"See https://mypy.readthedocs.io/en/stable/common_issues.html"
"#variables-vs-type-aliases",
base_expr,
code=codes.VALID_TYPE,
)
msg = "Invalid base class"
if name_repr:
msg += f' "{name_repr}"'
self.fail(msg, base_expr)
if isinstance(base_expr, IndexExpr):
index_value = self.index_expr_int_value(base_expr.index)
if index_value is not None:
# Mirror the hint semantic analysis reports for an
# integer used as a type argument.
self.fail(
f"Invalid type: try using Literal[{index_value}] instead?",
base_expr,
code=codes.VALID_TYPE,
)

def is_valid_deferred_base_type(self, typ: ProperType) -> bool:
"""Can a deferred base class have this inferred type?

Mirror what semantic analysis accepts for an explicitly typed
variable: ``Any`` from an explicit annotation, a type object, or a
union of those (e.g. ``type | Any``, when an ambiguous tuple index
may land on either). Anything else is rejected.
"""
if isinstance(typ, UnionType):
return all(
self.is_valid_deferred_base_type(get_proper_type(item)) for item in typ.items
)
return (
self.is_explicit_any(typ)
or (isinstance(typ, Instance) and typ.type.fullname == "builtins.type")
or (isinstance(typ, TypeType) and self.is_explicit_any(typ.item))
)

@staticmethod
def index_expr_int_value(index: Expression) -> int | None:
"""Value of a constant integer index expression, or None."""
value = constant_fold_expr(index, "<unused>")
if isinstance(value, int) and not isinstance(value, bool):
return value
return None
Comment on lines +2961 to +2966

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you are implementing brittle handling for only the simple cases of IndexExpr being an IntExpr or an int primitive modified by a negative sign, instead of handling general Expressions.

Consider adding test cases where args[1] is type Any and confirming that e.g. args[-(-1)] also works in that case, for example by adding another 2 lines to your existing test testSubclassAnyFromNestedUnpackedUnboundedStarArgsIndexInsideUnambiguous to add another Sub class using one of the args entries as its base, but using a slightly more complicated double negative expression as the IndexExpr:

def from_unpacked2(*args: *tuple[*tuple[Any, ...]]) -> None:
         pass
     class Sub2(args[1]):
         pass
+    class Sub3(args[-(-1)]):
+        pass

Currently your latest version fails such a check but I think we would expect such to succeed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 4e6dc32 .


def deferred_index_base_type(self, var: Var, base_expr: IndexExpr) -> ProperType | None:
"""Type of a deferred subscripted base class, e.g. ``args[1]``.

``var`` is the subscripted variable. Return the type of the item at
the subscript, or None if it cannot be determined statically; the
base class is then rejected, as it was before deferred validation.
Only a constant integer index is supported, and only into a tuple
or sequence type, mirroring what can be known about the item type.
"""
if not isinstance(base_expr.base, NameExpr):
return None
index_value = self.index_expr_int_value(base_expr.index)
if index_value is None:
return None
var_type = get_proper_type(var.type) if var.type is not None else None
if isinstance(var_type, TupleType):
return self.tuple_item_at_index(var_type, index_value)
if isinstance(var_type, Instance):
if var_type.type.fullname in TUPLE_NAMES and var_type.args:
# Homogeneous tuple[T, ...]: every item has type T.
return get_proper_type(var_type.args[0])
sequence_info = var_type.type.get_base("typing.Sequence")
if sequence_info is not None:
mapped = map_instance_to_supertype(var_type, sequence_info)
if mapped.args:
return get_proper_type(mapped.args[0])
return None

def tuple_item_at_index(self, tuple_type: TupleType, index: int) -> ProperType | None:
"""Type of a tuple item at a constant index, or None if ambiguous.

If the tuple type has a variadic (unpacked) item, an index may land
on more than one item. Then the union of the items it may land on
is returned, and the caller decides whether that union is an
acceptable base class type. A single fixed item (or ``Any``, if
every candidate is ``Any``) is returned directly.
"""
items = [get_proper_type(item) for item in flatten_nested_tuples(tuple_type.items)]
unpack_index = find_unpack_in_list(items)
if unpack_index is None:
pos = index if index >= 0 else index + len(items)
if 0 <= pos < len(items):
return items[pos]
return None
unpack_item = items[unpack_index]
assert isinstance(unpack_item, UnpackType)
unpacked = get_proper_type(unpack_item.type)
if not (
isinstance(unpacked, Instance)
and unpacked.type.fullname in TUPLE_NAMES
and unpacked.args
):
return None
variadic_item = get_proper_type(unpacked.args[0])
prefix = items[:unpack_index]
suffix = items[unpack_index + 1 :]
possible: list[ProperType]
if index >= 0:
if index < len(prefix):
possible = [prefix[index]]
else:
possible = [variadic_item]
# A suffix item sits at position len(prefix) + <variadic
# length> + k, and the variadic length is unknown, so it may
# also land on this index.
for k, item in enumerate(suffix):
if len(prefix) + k <= index:
possible.append(item)
else:
from_end = -index
if from_end <= len(suffix):
possible = [suffix[len(suffix) - from_end]]
else:
possible = [variadic_item]
# A prefix item sits at distance len(suffix) + <variadic
# length> + (len(prefix) - k) from the end.
for k, item in enumerate(prefix):
if len(suffix) + (len(prefix) - k) <= from_end:
possible.append(item)
if all(isinstance(item, AnyType) for item in possible):
return possible[0]
Comment on lines +3047 to +3048

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about if there are a mix of possible type (or Any) instances? That should also be supported, right?

E.g. I think the following should pass (reveal_type can be omitted in the below but I include it to help explain why it should pass):

def across_the_unpack_ambiguous_with_type_should_be_fine(*args: *tuple[type, *tuple[Any, ...], int]) -> None:
    reveal_type(args[-2]) # N: Revealed type is "builtins.type | Any"
    class Sub(args[-2]):
      pass

def across_the_unpack_ambiguous_with_type_should_be_fine2(*args: *tuple[int, *tuple[Any, ...], type]) -> None:
    reveal_type(args[1]) # N: Revealed type is "Any | builtins.type"
    class Sub(args[1]):
      pass

(Note builtins.type might be just type in e.g. the MyPy Playground, it depends on the test configuration; only affects the reveal_type step which is just for clarity, even without those lines, the class Sub lines should pass.)

Noting that in general variables of type type should be able to be subclassed, e.g.
https://mypy-play.net/?mypy=2.4.0&python=3.12&gist=e92cca9c89d683d7b8a93e6640826ead

# passes on MyPy 2.4.0
# derived from example given in https://github.com/python/mypy/issues/5865#issuecomment-489446397 which they fixed to pass
def subclass(a_type: type) -> type:
    class A(a_type): pass
    return A

So given that type can be subclassed and Any can be subclassed, type | Any should be able to be subclassed, or type_a | type_b | Any or just type_a | type_b.

if len(possible) == 1:
return possible[0]
# The index may land on several items; return their union and let
# the caller decide whether it is an acceptable base class type.
return make_simplified_union(possible)

def visit_class_def(self, defn: ClassDef) -> None:
"""Type check a class definition."""
typ = defn.info
self.check_deferred_base_classes(defn)
for base in typ.mro[1:]:
if base.is_final:
self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn)
Expand Down
19 changes: 19 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from collections import defaultdict
from collections.abc import Callable, Iterator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum, unique
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -3631,6 +3632,18 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
EXCLUDED_ENUM_ATTRIBUTES: Final = frozenset({"_ignore_", "_order_", "__order__"})


@dataclass
class DeferredBaseClass:
"""A base class that is a variable whose type was not yet inferred.

The type checker validates these once the variable's type is known.
"""

var: Var
expr: Expression
defining_literal: bool


class TypeInfo(SymbolNode):
"""The type structure of a single class.

Expand Down Expand Up @@ -3690,6 +3703,7 @@ class is generic then it will be a type constructor of higher kind.
"type_object_type",
"default_depends",
"typeddict_data",
"deferred_base_classes",
)

_fullname: str # Fully qualified name
Expand Down Expand Up @@ -3924,6 +3938,11 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None
self.dataclass_transform_spec = None
self.is_type_check_only = False
self.deprecated = None
# Base classes that are variables whose types had not been inferred yet
# during semantic analysis. The type checker validates them once the
# types are known (see TypeChecker.check_deferred_base_classes).
# This is not serialized; it is only meaningful within a single build.
self.deferred_base_classes: list[DeferredBaseClass] = []
self.type_object_type = None
self.default_depends = {}
self.typeddict_data = None
Expand Down
Loading
Loading