Skip to content

Commit 7b928ed

Browse files
committed
Fix type narrowing for @Final classes with identity checks
Fixes #20590 When using 'is' operator or match/case patterns to check type objects of @Final classes, mypy now properly narrows the types since @Final classes cannot have subclasses at runtime. Changes: - typeops.py: Updated is_singleton_identity_type() to recognize CallableType and TypeType representing @Final class constructors as singleton types for identity-based narrowing - checker.py: Modified narrowing logic to: 1. Allow @Final class type objects in narrowable_operand check 2. Preserve else-branch narrowing for @Final class identity checks since no subclasses can exist This enables exhaustiveness checking with assert_never() when all @Final class cases are handled in if/elif or match/case branches.
1 parent 639fcde commit 7b928ed

File tree

2 files changed

+17
-1
lines changed

2 files changed

+17
-1
lines changed

mypy/checker.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6567,6 +6567,7 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa
65676567
and not (
65686568
isinstance(p_expr := get_proper_type(expr_type), CallableType)
65696569
and p_expr.is_type_obj()
6570+
and not p_expr.type_object().is_final
65706571
)
65716572
):
65726573
h = literal_hash(expr)
@@ -6803,7 +6804,12 @@ def narrow_type_by_equality(
68036804
operands[i], *conditional_types(expr_type, [target])
68046805
)
68056806
if if_map:
6806-
else_map = {} # this is the big difference compared to the above
6807+
# For final classes, we can narrow in the else branch too since
6808+
# no subclasses can exist. Otherwise, clear the else_map.
6809+
target_type = get_proper_type(target.item)
6810+
if not (isinstance(target_type, CallableType) and target_type.is_type_obj()
6811+
and target_type.type_object().is_final):
6812+
else_map = {}
68076813
partial_type_maps.append((if_map, else_map))
68086814

68096815
# We will not have duplicate entries in our type maps if we only have two operands,

mypy/typeops.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,16 @@ def is_singleton_identity_type(typ: ProperType) -> bool:
10041004
)
10051005
if isinstance(typ, LiteralType):
10061006
return typ.is_enum_literal() or isinstance(typ.value, bool)
1007+
# Check if this is a type object of a final class
1008+
if isinstance(typ, TypeType):
1009+
item = typ.item
1010+
if isinstance(item, Instance) and item.type.is_final:
1011+
return True
1012+
# Check if this is a callable representing a final class constructor
1013+
if isinstance(typ, CallableType) and typ.is_type_obj():
1014+
type_obj = typ.type_object()
1015+
if type_obj.is_final:
1016+
return True
10071017
return False
10081018

10091019

0 commit comments

Comments
 (0)