Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 44 additions & 14 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,7 @@
FineGrainedDeferredNodeType: _TypeAlias = Union[FuncDef, MypyFile, OverloadedFuncDef]


# A node which is postponed to be processed during the next pass.
# In normal mode one can defer functions and methods (also decorated and/or overloaded)
# and lambda expressions. Nested functions can't be deferred -- only top-level functions
# and methods of classes not defined within a function can be deferred.

class DeferredNode(NamedTuple):
node: DeferredNodeType
# And its TypeInfo (for semantic analysis self type handling
Expand Down Expand Up @@ -2426,16 +2423,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type

# Special case: only non-abstract non-protocol classes can be assigned to
# variables with explicit type Type[A], where A is protocol or abstract.
rvalue_type = get_proper_type(rvalue_type)
lvalue_type = get_proper_type(lvalue_type)
if (isinstance(rvalue_type, CallableType) and rvalue_type.is_type_obj() and
(rvalue_type.type_object().is_abstract or
rvalue_type.type_object().is_protocol) and
isinstance(lvalue_type, TypeType) and
isinstance(lvalue_type.item, Instance) and
(lvalue_type.item.type.is_abstract or
lvalue_type.item.type.is_protocol)):
self.msg.concrete_only_assign(lvalue_type, rvalue)
if not self.check_concrete_only_assign(lvalue_type, lvalue, rvalue_type, rvalue):
return
if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType):
# Don't use type binder for definitions of special forms, like named tuples.
Expand All @@ -2453,6 +2441,48 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue)
self.check_assignment_to_slots(lvalue)

def check_concrete_only_assign(self,
lvalue_type: Optional[Type],
lvalue: Expression,
rvalue_type: Type,
rvalue: Expression) -> bool:
Comment on lines +2444 to +2448
Copy link

Choose a reason for hiding this comment

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

Method Parameter Ordering

Parameter ordering mixes types and expressions inconsistently (lvalue_type, lvalue, rvalue_type, rvalue). Grouping related parameters together (types together, expressions together) would improve method signature readability and follow consistent parameter organization patterns.

Standards
  • Clean-Code-Functions
  • Maintainability-Quality-Readability

Comment on lines +2444 to +2448
Copy link

Choose a reason for hiding this comment

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

Parameter Organization Inconsistency

Parameter ordering mixes types and expressions inconsistently reducing method signature readability. Grouping related parameters together would improve comprehension and follow consistent organization patterns. Current ordering makes parameter relationships unclear.

Standards
  • Clean-Code-Functions
  • Maintainability-Quality-Readability

if (isinstance(lvalue, NameExpr) and isinstance(rvalue, NameExpr)
and lvalue.node == rvalue.node):
# This means that we reassign abstract class to itself. Like `A = A`
Comment on lines +2449 to +2451
Copy link

Choose a reason for hiding this comment

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

Self Assignment Detection

Self-assignment detection relies on node identity comparison which may not handle all edge cases like aliased references or complex expressions. While the current implementation prevents false positives for direct self-assignment, it could miss security-relevant reassignments through aliases. Consider expanding validation to handle indirect references that could bypass abstract class assignment restrictions.

Standards
  • CWE-670
  • OWASP-A04

return True

# Types should already be proper types from the caller
# Skip processing for PartialType to avoid assertion failures
if isinstance(lvalue_type, PartialType):
return True
Comment on lines +2456 to +2457
Copy link

Choose a reason for hiding this comment

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

PartialType Crash Prevention

Early return for PartialType prevents assertion failures in get_proper_type() calls that would crash the type checker. This addresses critical reliability issue where partially inferred types during branch analysis could cause complete checker failure. Performance impact includes avoiding expensive crash recovery and maintaining system stability.

        # Skip processing for PartialType to avoid assertion failures
        if isinstance(lvalue_type, PartialType) or lvalue_type is None:
            return True
Commitable Suggestion
Suggested change
if isinstance(lvalue_type, PartialType):
return True
# Skip processing for PartialType to avoid assertion failures
if isinstance(lvalue_type, PartialType) or lvalue_type is None:
return True
Standards
  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • ISO-IEC-25010-Performance-Efficiency-Time-Behavior
  • Algorithmic-Complexity-Early-Exit-Optimization

Comment on lines +2454 to +2457
Copy link

Choose a reason for hiding this comment

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

Redundant Type Processing

Comment indicates types should already be proper types from caller, yet original code performed get_proper_type() calls that were removed. This violates organization guideline requiring code reuse and redundancy avoidance. The refactoring assumes caller responsibility without enforcing the contract through preconditions.

Standards
  • Org-Guideline-Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application
  • ISO-IEC-25010-Reliability-Maturity
  • DbC-Preconditions

if not (
isinstance(rvalue_type, CallableType) and
rvalue_type.is_type_obj() and
(rvalue_type.type_object().is_abstract or
rvalue_type.type_object().is_protocol)):
return True

lvalue_is_a_type = (
isinstance(lvalue_type, TypeType) and
isinstance(lvalue_type.item, Instance) and
(lvalue_type.item.type.is_abstract or
lvalue_type.item.type.is_protocol)
)

lvalue_is_a_callable = False
if isinstance(lvalue_type, CallableType):
ret_type = get_proper_type(lvalue_type.ret_type)
lvalue_is_a_callable = (
isinstance(ret_type, Instance) and
(ret_type.type.is_abstract or ret_type.type.is_protocol)
)
Comment on lines +2472 to +2478

Choose a reason for hiding this comment

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

medium

This block can be slightly refactored to improve readability by nesting the isinstance check. This makes the logic a little more explicit and avoids a long boolean expression.

Suggested change
lvalue_is_a_callable = False
if isinstance(lvalue_type, CallableType):
ret_type = get_proper_type(lvalue_type.ret_type)
lvalue_is_a_callable = (
isinstance(ret_type, Instance) and
(ret_type.type.is_abstract or ret_type.type.is_protocol)
)
lvalue_is_a_callable = False
if isinstance(lvalue_type, CallableType):
ret_type = get_proper_type(lvalue_type.ret_type)
if isinstance(ret_type, Instance):
lvalue_is_a_callable = (
ret_type.type.is_abstract or ret_type.type.is_protocol
)

Comment on lines +2472 to +2478
Copy link

Choose a reason for hiding this comment

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

Callable Logic Enhancement

Callable validation logic only checks return type for abstract/protocol status but ignores callable parameter types. Complete callable validation should verify all parameter types for abstract/protocol constraints. This creates incomplete validation coverage for callable type assignments.

Standards
  • Algorithm-Correctness-Completeness
  • Business-Rule-Type-Validation
  • Logic-Verification-Coverage

Comment on lines +2472 to +2478
Copy link

Choose a reason for hiding this comment

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

Incomplete Callable Validation

Callable validation logic only examines return type for abstract/protocol constraints but ignores parameter types. Complete validation requires checking all parameter types for abstract/protocol status. This creates logical gaps where callable assignments with abstract parameter types bypass validation checks.

        lvalue_is_a_callable = False
        if isinstance(lvalue_type, CallableType):
            ret_type = get_proper_type(lvalue_type.ret_type)
            # Check return type for abstract/protocol status
            lvalue_is_a_callable = (
                isinstance(ret_type, Instance) and
                (ret_type.type.is_abstract or ret_type.type.is_protocol)
            )
            # Also check parameter types for abstract/protocol status
            if not lvalue_is_a_callable:
                for arg_type in lvalue_type.arg_types:
                    arg_type = get_proper_type(arg_type)
                    if (isinstance(arg_type, Instance) and
                            (arg_type.type.is_abstract or arg_type.type.is_protocol)):
                        lvalue_is_a_callable = True
                        break
Commitable Suggestion
Suggested change
lvalue_is_a_callable = False
if isinstance(lvalue_type, CallableType):
ret_type = get_proper_type(lvalue_type.ret_type)
lvalue_is_a_callable = (
isinstance(ret_type, Instance) and
(ret_type.type.is_abstract or ret_type.type.is_protocol)
)
lvalue_is_a_callable = False
if isinstance(lvalue_type, CallableType):
ret_type = get_proper_type(lvalue_type.ret_type)
# Check return type for abstract/protocol status
lvalue_is_a_callable = (
isinstance(ret_type, Instance) and
(ret_type.type.is_abstract or ret_type.type.is_protocol)
)
# Also check parameter types for abstract/protocol status
if not lvalue_is_a_callable:
for arg_type in lvalue_type.arg_types:
arg_type = get_proper_type(arg_type)
if (isinstance(arg_type, Instance) and
(arg_type.type.is_abstract or arg_type.type.is_protocol)):
lvalue_is_a_callable = True
break
Standards
  • Business-Rule-Validation-Completeness
  • Logic-Verification-Coverage
  • Algorithm-Correctness-Completeness


Comment on lines +2472 to +2479
Copy link

Choose a reason for hiding this comment

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

Incomplete Validation Logic

Callable validation only checks return type for abstract/protocol status but ignores parameter types. Complete validation should verify all parameter types for abstract/protocol constraints. This creates incomplete coverage for callable type assignments.

Standards
  • SOLID-ISP
  • Clean-Code-Functions
  • Maintainability-Quality-Completeness

if lvalue_is_a_type or lvalue_is_a_callable:
# `lvalue_type` here is either `TypeType` or `CallableType`:
self.msg.concrete_only_assign(cast(Type, lvalue_type), rvalue)
Copy link

Choose a reason for hiding this comment

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

Type Cast Safety

Type cast to Type without runtime validation could cause ClassCastException if lvalue_type is not actually a Type instance. The cast assumes type safety based on conditional checks but doesn't handle edge cases where type system assumptions fail. Adding runtime type validation before cast would prevent potential runtime failures.

Standards
  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • ISO-IEC-25010-Functional-Correctness-Appropriateness
  • DbC-Preconditions

Copy link

Choose a reason for hiding this comment

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

Unsafe Type Cast

Type cast to Type without runtime validation assumes type safety based on conditional checks but doesn't handle edge cases where type system assumptions fail. The cast could cause runtime failures if lvalue_type is not actually a Type instance despite passing the conditional guards.

Standards
  • Algorithm-Correctness-Type-Safety
  • Logic-Verification-Runtime-Safety
  • Business-Rule-Defensive-Programming

Copy link

Choose a reason for hiding this comment

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

Unsafe Type Cast

Type cast to Type without runtime validation assumes type safety based on conditional checks but doesn't handle edge cases where type system assumptions fail. The cast could cause runtime failures if lvalue_type is not actually a Type instance despite passing conditional guards.

            # Handle each type case separately to avoid unsafe casting
            if lvalue_is_a_type:
                self.msg.concrete_only_assign(lvalue_type, rvalue)
            elif lvalue_is_a_callable:
                self.msg.concrete_only_assign(lvalue_type, rvalue)
Commitable Suggestion
Suggested change
self.msg.concrete_only_assign(cast(Type, lvalue_type), rvalue)
# Handle each type case separately to avoid unsafe casting
if lvalue_is_a_type:
self.msg.concrete_only_assign(lvalue_type, rvalue)
elif lvalue_is_a_callable:
self.msg.concrete_only_assign(lvalue_type, rvalue)
Standards
  • CWE-704
  • CWE-843
  • OWASP-A04

Copy link

Choose a reason for hiding this comment

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

Unsafe Type Cast

Type cast to Type without runtime validation assumes type safety based on conditional checks. The cast could cause runtime failures if lvalue_type is not actually a Type instance despite passing conditional guards. Logic assumes perfect type system alignment without defensive validation.

Standards
  • Algorithm-Correctness-Type-Safety
  • Logic-Verification-Runtime-Safety
  • Business-Rule-Defensive-Programming

return False
return True
Comment on lines +2444 to +2484
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore alias handling before concrete-only enforcement

Factoring the concrete-only check into check_concrete_only_assign() dropped the get_proper_type() normalization we used to do in-line. As a result, when the lvalue (or rvalue) type arrives wrapped in a TypeAliasType, the new helper never recognizes it as a TypeType/CallableType, so the concrete-only guard silently stops running. For example:

from typing import Type, TypeAlias
from abc import ABC

class Base(ABC): ...
Concrete = type("Concrete", (Base,), {})

AbstractType: TypeAlias = Type[Base]
x: AbstractType = Base  # used to error, now passes

To keep the previous behaviour, normalize both sides after the PartialType guard so aliases (and other wrappers) are unwrapped before the isinstance checks:

         if isinstance(lvalue_type, PartialType):
             return True
-        if not (
-            isinstance(rvalue_type, CallableType) and
-            rvalue_type.is_type_obj() and
-            (rvalue_type.type_object().is_abstract or
-                rvalue_type.type_object().is_protocol)):
+        proper_rvalue_type = get_proper_type(rvalue_type)
+        if not (
+            isinstance(proper_rvalue_type, CallableType) and
+            proper_rvalue_type.is_type_obj() and
+            (proper_rvalue_type.type_object().is_abstract or
+                proper_rvalue_type.type_object().is_protocol)):
             return True
 
-        lvalue_is_a_type = (
-            isinstance(lvalue_type, TypeType) and
-            isinstance(lvalue_type.item, Instance) and
-            (lvalue_type.item.type.is_abstract or
-                lvalue_type.item.type.is_protocol)
+        proper_lvalue_type = get_proper_type(lvalue_type)
+
+        lvalue_is_a_type = (
+            isinstance(proper_lvalue_type, TypeType) and
+            isinstance(proper_lvalue_type.item, Instance) and
+            (proper_lvalue_type.item.type.is_abstract or
+                proper_lvalue_type.item.type.is_protocol)
         )
 
         lvalue_is_a_callable = False
-        if isinstance(lvalue_type, CallableType):
-            ret_type = get_proper_type(lvalue_type.ret_type)
+        if isinstance(proper_lvalue_type, CallableType):
+            ret_type = get_proper_type(proper_lvalue_type.ret_type)
             lvalue_is_a_callable = (
                 isinstance(ret_type, Instance) and
                 (ret_type.type.is_abstract or ret_type.type.is_protocol)
             )
 
         if lvalue_is_a_type or lvalue_is_a_callable:
             # `lvalue_type` here is either `TypeType` or `CallableType`:
-            self.msg.concrete_only_assign(cast(Type, lvalue_type), rvalue)
+            self.msg.concrete_only_assign(cast(Type, proper_lvalue_type), rvalue)
             return False

That reinstates the original guard for alias-backed annotations.

🤖 Prompt for AI Agents
In mypy/checker.py around lines 2444 to 2484, the new helper dropped
normalization with get_proper_type(), so TypeAliasType (and other wrappers) can
bypass the concrete-only enforcement; after the PartialType guard, call
get_proper_type() on both lvalue_type and rvalue_type (e.g. assign lvalue_type =
get_proper_type(lvalue_type) and rvalue_type = get_proper_type(rvalue_type))
before the isinstance checks so TypeAliasType/other wrappers are unwrapped and
the subsequent TypeType/CallableType logic runs as before.


# (type, operator) tuples for augmented assignments supported with partial types
partial_type_augmented_ops: Final = {
('builtins.list', '+'),
Expand Down
32 changes: 32 additions & 0 deletions test-data/unit/check-abstract.test
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,38 @@ if int():
var_old = C # OK
[out]

[case testInstantiationAbstractsWithCallables]
from typing import Callable, Type
from abc import abstractmethod

class A:
@abstractmethod
def m(self) -> None: pass
class B(A): pass
class C(B):
def m(self) -> None:
pass

var: Callable[[], A]
var() # OK

var = A # E: Can only assign concrete classes to a variable of type "Callable[[], A]"
var = B # E: Can only assign concrete classes to a variable of type "Callable[[], A]"
var = C # OK

# Type aliases:
A1 = A
B1 = B
C1 = C
var = A1 # E: Can only assign concrete classes to a variable of type "Callable[[], A]"
var = B1 # E: Can only assign concrete classes to a variable of type "Callable[[], A]"
var = C1 # OK

# Self assign:
A = A # OK
B = B # OK
C = C # OK

[case testInstantiationAbstractsInTypeForClassMethods]
from typing import Type
from abc import abstractmethod
Expand Down
30 changes: 30 additions & 0 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,36 @@ if int():
var_old = B # OK
var_old = C # OK

[case testInstantiationProtocolWithCallables]
from typing import Callable, Protocol

class P(Protocol):
def m(self) -> None: pass
class B(P): pass
class C:
def m(self) -> None:
pass

var: Callable[[], P]
var() # OK

var = P # E: Can only assign concrete classes to a variable of type "Callable[[], P]"
var = B # OK
var = C # OK

# Type aliases:
P1 = P
B1 = B
C1 = C
var = P1 # E: Can only assign concrete classes to a variable of type "Callable[[], P]"
var = B1 # OK
var = C1 # OK

# Self assign:
P = P # OK
B = B # OK
C = C # OK

[case testInstantiationProtocolInTypeForClassMethods]
from typing import Type, Protocol

Expand Down