From 7b928ed5401c3ed4c491c16be272ede0a2ed4ba7 Mon Sep 17 00:00:00 2001 From: shiva-sai-824 Date: Thu, 22 Jan 2026 01:49:32 +0530 Subject: [PATCH 1/2] 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. --- mypy/checker.py | 8 +++++++- mypy/typeops.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 452cae0206fe..d837e0df356f 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,12 @@ 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 From 51d24c2d6f5cee7a0396dfec5fa6f8869c98f0c6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:39:49 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d837e0df356f..fb969ad0e017 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6807,8 +6807,11 @@ def narrow_type_by_equality( # 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): + 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))