diff --git a/mypy/checker.py b/mypy/checker.py index 452cae0206fe..fb969ad0e017 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6567,6 +6567,7 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa and not ( isinstance(p_expr := get_proper_type(expr_type), CallableType) and p_expr.is_type_obj() + and not p_expr.type_object().is_final ) ): h = literal_hash(expr) @@ -6803,7 +6804,15 @@ def narrow_type_by_equality( operands[i], *conditional_types(expr_type, [target]) ) if if_map: - else_map = {} # this is the big difference compared to the above + # For final classes, we can narrow in the else branch too since + # no subclasses can exist. Otherwise, clear the else_map. + target_type = get_proper_type(target.item) + if not ( + isinstance(target_type, CallableType) + and target_type.is_type_obj() + and target_type.type_object().is_final + ): + else_map = {} partial_type_maps.append((if_map, else_map)) # We will not have duplicate entries in our type maps if we only have two operands, diff --git a/mypy/typeops.py b/mypy/typeops.py index 00431f02fa5e..0052d976de1b 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1004,6 +1004,16 @@ def is_singleton_identity_type(typ: ProperType) -> bool: ) if isinstance(typ, LiteralType): return typ.is_enum_literal() or isinstance(typ.value, bool) + # Check if this is a type object of a final class + if isinstance(typ, TypeType): + item = typ.item + if isinstance(item, Instance) and item.type.is_final: + return True + # Check if this is a callable representing a final class constructor + if isinstance(typ, CallableType) and typ.is_type_obj(): + type_obj = typ.type_object() + if type_obj.is_final: + return True return False