Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 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
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
83 changes: 83 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,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 +221,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,
infer_class_variances,
Expand Down Expand Up @@ -2807,9 +2809,90 @@ 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.
"""
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
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_explicit_any(typ)
or (isinstance(typ, Instance) and typ.type.fullname == "builtins.type")
or (isinstance(typ, TypeType) and self.is_explicit_any(typ.item))
)
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)
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)

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 @@ -3627,6 +3628,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 @@ -3686,6 +3699,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 @@ -3920,6 +3934,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
37 changes: 29 additions & 8 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
ContinueStmt,
DataclassTransformSpec,
Decorator,
DeferredBaseClass,
DelStmt,
DictExpr,
DictionaryComprehension,
Expand Down Expand Up @@ -243,6 +244,7 @@
from mypy.tvar_scope import TypeVarLikeScope
from mypy.typeanal import (
SELF_TYPE_NAMES,
DeferredBaseClassVar,
FindTypeVarVisitor,
TypeAnalyser,
TypeVarDefaultTranslator,
Expand Down Expand Up @@ -2034,7 +2036,7 @@ def analyze_class(self, defn: ClassDef) -> None:
self.mark_incomplete(defn.name, defn)
return

base_types, base_error = bases_result
base_types, base_error, deferred_bases = bases_result
if any(isinstance(base, PlaceholderType) for base, _ in base_types):
# We need to know the TypeInfo of each base to construct the MRO. Placeholder types
# are okay in nested positions, since they can't affect the MRO.
Expand Down Expand Up @@ -2071,6 +2073,7 @@ def analyze_class(self, defn: ClassDef) -> None:
defn.info.default_depends = default_depends
if base_error:
defn.info.fallback_to_any = True
defn.info.deferred_base_classes = deferred_bases
if any_meta:
defn.info.meta_fallback_to_any = True

Expand Down Expand Up @@ -2622,17 +2625,23 @@ def get_name_repr_of_expr(self, expr: Expression) -> str | None:

def analyze_base_classes(
self, cls_name: str, base_type_exprs: list[Expression]
) -> tuple[list[tuple[ProperType, Expression]], bool] | None:
) -> tuple[list[tuple[ProperType, Expression]], bool, list[DeferredBaseClass]] | None:
"""Analyze base class types.

Return None if some definition was incomplete. Otherwise, return a tuple
with these items:

* List of (analyzed type, original expression) tuples
* Boolean indicating whether one of the bases had a semantic analysis error
* List of DeferredBaseClass for bases whose validity could not be
checked yet, because the variable's type has not been inferred
(inference happens in the type checker). These are provisionally
treated as having an Any base; the type checker validates them once
the type is known.
"""
is_error = False
bases = []
deferred: list[DeferredBaseClass] = []
for i, base_expr in enumerate(base_type_exprs):
if (
isinstance(base_expr, RefExpr)
Expand All @@ -2656,6 +2665,13 @@ def analyze_base_classes(
allow_type_any=True,
unique_name=inline_base(cls_name, i),
)
except DeferredBaseClassVar as e:
# The base is a variable whose type will only be known after
# type checking (e.g. `x = args[1]` where `args[1]` is `Any`).
# Provisionally treat it as Any; the type checker verifies the
# inferred type (see TypeChecker.check_deferred_base_classes).
deferred.append(DeferredBaseClass(e.var, base_expr, e.defining_literal))
base = AnyType(TypeOfAny.special_form)
except TypeTranslationError:
name = self.get_name_repr_of_expr(base_expr)
if isinstance(base_expr, CallExpr):
Expand All @@ -2671,7 +2687,7 @@ def analyze_base_classes(
return None
base = get_proper_type(base)
bases.append((base, base_expr))
return bases, is_error
return bases, is_error, deferred

def configure_base_classes(
self, defn: ClassDef, bases: list[tuple[ProperType, Expression]]
Expand All @@ -2695,11 +2711,16 @@ def configure_base_classes(
base_types.append(base)
elif isinstance(base, AnyType):
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)
# A deferred base class is provisionally treated as Any;
# the type checker reports this error once the actual
# inferred type is known, if it is Any.
is_deferred = any(d.expr is base_expr for d in defn.info.deferred_base_classes)
if not is_deferred:
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)
info.fallback_to_any = True
elif isinstance(base, TypedDictType):
base_types.append(base.fallback)
Expand Down
30 changes: 30 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,30 @@ def analyze_type_alias(
return res, analyzer.aliases_used


class DeferredBaseClassVar(Exception):
"""A base class is a variable whose type hasn't been inferred yet.

Semantic analysis runs before type checking, so when a class uses a variable
with an inferred (not declared) type as a base class -- e.g. ``x = args[1]``
where ``args[1]`` is ``Any`` -- the variable's type is not yet known. The
class is provisionally treated as having an ``Any`` base; the type checker
validates the inferred type once it is known (see
``TypeChecker.check_deferred_base_classes``).

The variable is carried on the exception so the checker can inspect its
inferred type without re-resolving the base class expression.
"""

def __init__(self, var: nodes.Var, defining_literal: bool) -> None:
super().__init__()
self.var = var
# Whether the base class expression was being analyzed as part of a
# Literal[...] type. Semantic analysis does not report "not valid as a
# type" for variables in that position, so the type checker must not
# either.
self.defining_literal = defining_literal


class TypeAnalyser(SyntheticTypeVisitor[Type], TypeAnalyzerPluginInterface):
"""Semantic analyzer for types.

Expand Down Expand Up @@ -1018,6 +1042,12 @@ def analyze_unbound_type_without_type_info(
return AnyType(TypeOfAny.special_form)
if isinstance(typ, TypeType) and isinstance(typ.item, AnyType):
return AnyType(TypeOfAny.from_another_any, source_any=typ.item)
if typ is None:
# The variable's type has not been inferred yet (inference
# happens in the type checker, after semantic analysis).
# Defer the validity check; the type checker will verify
# the inferred type. Provisionally treat the base as Any.
raise DeferredBaseClassVar(sym.node, defining_literal)
# Option 2:
# Unbound type variable. Currently these may be still valid,
# for example when defining a generic type alias.
Expand Down
Loading
Loading