From 5910e0de8a197c9f6b9ab402c321c0673d5061ba Mon Sep 17 00:00:00 2001 From: user Date: Fri, 14 Nov 2025 15:39:12 -0500 Subject: [PATCH 1/6] Add redundant-annotation warning Relates to #18540 --- docs/source/command_line.rst | 5 +++++ docs/source/error_code_list2.rst | 17 +++++++++++++++++ mypy/checker.py | 28 ++++++++++++++++++++++++++++ mypy/errorcodes.py | 3 +++ mypy/main.py | 7 +++++++ mypy/messages.py | 7 +++++++ mypy/options.py | 3 +++ test-data/unit/check-warnings.test | 27 +++++++++++++++++++++++++++ 8 files changed, 97 insertions(+) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 5efec6855593..dd61c6775bc5 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -477,6 +477,11 @@ Configuring warnings The following flags enable warnings for code that is sound but is potentially problematic or redundant in some way. +.. option:: --warn-redundant-annotation + + This flag will make mypy report an error whenever your code uses + an unnecessary annotation in an assignment that can safely be removed. + .. option:: --warn-redundant-casts This flag will make mypy report an error whenever your code uses diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index bd2436061974..8a3f477e5c55 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -66,6 +66,23 @@ Example: def __init__(self) -> None: self.value = 0 +.. _code-redundant-annotation: + +Check that annotation is not redundant [redundant-annotation] +------------------------------------------------------------- + +If you use :option:`--warn-redundant-annotation `, mypy will generate an error if the +annotation type is the same as the inferred type. + +Example: + +.. code-block:: python + + # mypy: warn-redundant-annotation + + # Error: Annotation "int" is redundant [redundant-annotation] + count: int = 4 + .. _code-redundant-cast: Check that cast is not redundant [redundant-cast] diff --git a/mypy/checker.py b/mypy/checker.py index 07f5c520de95..32c378389ba9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3206,6 +3206,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: Handle all kinds of assignment statements (simple, indexed, multiple). """ + self.check_redundant_annotation(s) + # Avoid type checking type aliases in stubs to avoid false # positives about modern type syntax available in stubs such # as X | Y. @@ -3258,6 +3260,32 @@ def check_type_alias_rvalue(self, s: AssignmentStmt) -> None: alias_type = self.expr_checker.accept(s.rvalue) self.store_type(s.lvalues[-1], alias_type) + def check_redundant_annotation(self, s: AssignmentStmt) -> None: + if ( + self.options.warn_redundant_annotation + and not s.is_final_def + and not s.is_alias_def + and s.unanalyzed_type is not None + and s.type is not None + and not is_same_type(s.type, AnyType(TypeOfAny.special_form)) + and is_same_type(s.type, self.expr_checker.accept(s.rvalue)) + ): + # skip ClassVar + if any( + isinstance(lvalue, NameExpr) + and isinstance(lvalue.node, Var) + and lvalue.node.is_classvar + for lvalue in s.lvalues + ): + return + + # skip dataclass and NamedTuple + cls = self.scope.active_class() + if cls and (dataclasses_plugin.is_processed_dataclass(cls) or cls.is_named_tuple): + return + + self.msg.redundant_annotation(s.type, s.rvalue) + def check_assignment( self, lvalue: Lvalue, diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 785b6166b18b..f2632c9feed6 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -162,6 +162,9 @@ def __hash__(self) -> int: "Disallow calling functions without type annotations from annotated functions", "General", ) +REDUNDANT_ANNOTATION: Final = ErrorCode( + "redundant-annotation", "Check that the annotation is necessary or can be omitted", "General" +) REDUNDANT_CAST: Final = ErrorCode( "redundant-cast", "Check that cast changes type of expression", "General" ) diff --git a/mypy/main.py b/mypy/main.py index 7d5721851c3d..35828dfedf65 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -820,6 +820,13 @@ def add_invertible_flag( title="Configuring warnings", description="Detect code that is sound but redundant or problematic.", ) + add_invertible_flag( + "--warn-redundant-annotation", + default=False, + strict_flag=False, + help="Warn when an annotation is the same as its inferred type", + group=lint_group, + ) add_invertible_flag( "--warn-redundant-casts", default=False, diff --git a/mypy/messages.py b/mypy/messages.py index 9fdfb748b288..5230248f6942 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1784,6 +1784,13 @@ def unsupported_type_type(self, item: Type, context: Context) -> None: f'Cannot instantiate type "type[{format_type_bare(item, self.options)}]"', context ) + def redundant_annotation(self, typ: Type, context: Context) -> None: + self.fail( + f"Annotation {format_type(typ, self.options)} is redundant (inferred type is the same)", + context, + code=codes.REDUNDANT_ANNOTATION, + ) + def redundant_cast(self, typ: Type, context: Context) -> None: self.fail( f"Redundant cast to {format_type(typ, self.options)}", diff --git a/mypy/options.py b/mypy/options.py index 39490c9f0bee..4665abc0118e 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -175,6 +175,9 @@ def __init__(self) -> None: # Also check typeshed for missing annotations self.warn_incomplete_stub = False + # Warn when an annotation is the same as its inferred type + self.warn_redundant_annotation = False + # Warn about casting an expression to its inferred type self.warn_redundant_casts = False diff --git a/test-data/unit/check-warnings.test b/test-data/unit/check-warnings.test index a2d201fa301d..f0a02ea3e61c 100644 --- a/test-data/unit/check-warnings.test +++ b/test-data/unit/check-warnings.test @@ -68,6 +68,33 @@ z = Any def f(q: Union[x, y, z]) -> None: cast(Union[x, y], q) +-- Redundant annotation +-- -------------------- + +[case testRedundantAnnotation] +# flags: --warn-redundant-annotation +a = 1 +b: int = a +[out] +main:3: error: Annotation "int" is redundant (inferred type is the same) + +[case testRedundantAnnotationSkips] +# flags: --warn-redundant-annotation +from dataclasses import dataclass +from typing import ClassVar, NamedTuple + +class a: + b: ClassVar[int] = 1 + c: ClassVar = 1 + +class d(NamedTuple): + e: int = 1 + +@dataclass +class f: + g: int = 1 +[builtins fixtures/tuple.pyi] + -- Unused 'type: ignore' comments -- ------------------------------ From a72ba7ac3a1f03d5038b7c1ceb5a20c328a4f7f0 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 15 Nov 2025 11:03:28 -0500 Subject: [PATCH 2/6] use is_class_var --- mypy/checker.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 32c378389ba9..e683b5775bbc 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -145,6 +145,7 @@ WithStmt, YieldExpr, get_func_def, + is_class_var, is_final_node, ) from mypy.operators import flip_ops, int_op_to_method, neg_ops @@ -3271,12 +3272,7 @@ def check_redundant_annotation(self, s: AssignmentStmt) -> None: and is_same_type(s.type, self.expr_checker.accept(s.rvalue)) ): # skip ClassVar - if any( - isinstance(lvalue, NameExpr) - and isinstance(lvalue.node, Var) - and lvalue.node.is_classvar - for lvalue in s.lvalues - ): + if any(isinstance(lvalue, NameExpr) and is_class_var(lvalue) for lvalue in s.lvalues): return # skip dataclass and NamedTuple From 593d4e097612e48b2169c443ae24f0ffa1bc09ea Mon Sep 17 00:00:00 2001 From: user Date: Sat, 15 Nov 2025 11:11:29 -0500 Subject: [PATCH 3/6] skip bare ClassVar --- mypy/checker.py | 8 ++++++-- test-data/unit/check-warnings.test | 14 ++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e683b5775bbc..5453802ebcda 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3271,8 +3271,12 @@ def check_redundant_annotation(self, s: AssignmentStmt) -> None: and not is_same_type(s.type, AnyType(TypeOfAny.special_form)) and is_same_type(s.type, self.expr_checker.accept(s.rvalue)) ): - # skip ClassVar - if any(isinstance(lvalue, NameExpr) and is_class_var(lvalue) for lvalue in s.lvalues): + # skip bare ClassVar + if ( + any(isinstance(lvalue, NameExpr) and is_class_var(lvalue) for lvalue in s.lvalues) + and isinstance(s.unanalyzed_type, UnboundType) + and not s.unanalyzed_type.args + ): return # skip dataclass and NamedTuple diff --git a/test-data/unit/check-warnings.test b/test-data/unit/check-warnings.test index f0a02ea3e61c..f45e20a7b04d 100644 --- a/test-data/unit/check-warnings.test +++ b/test-data/unit/check-warnings.test @@ -78,14 +78,20 @@ b: int = a [out] main:3: error: Annotation "int" is redundant (inferred type is the same) -[case testRedundantAnnotationSkips] +[case testRedundantAnnotationClassVar] # flags: --warn-redundant-annotation -from dataclasses import dataclass -from typing import ClassVar, NamedTuple +from typing import ClassVar class a: b: ClassVar[int] = 1 - c: ClassVar = 1 + c: ClassVar = "test" +[out] +main:5: error: Annotation "int" is redundant (inferred type is the same) + +[case testRedundantAnnotationSkips] +# flags: --warn-redundant-annotation +from dataclasses import dataclass +from typing import NamedTuple class d(NamedTuple): e: int = 1 From d851922cf9bfcb68487ad81a5d042a7e021a4a79 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 15 Nov 2025 11:21:52 -0500 Subject: [PATCH 4/6] correct context for --pretty --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5453802ebcda..35dae357ca77 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3284,7 +3284,7 @@ def check_redundant_annotation(self, s: AssignmentStmt) -> None: if cls and (dataclasses_plugin.is_processed_dataclass(cls) or cls.is_named_tuple): return - self.msg.redundant_annotation(s.type, s.rvalue) + self.msg.redundant_annotation(s.type, s.type) def check_assignment( self, From 1082552fbdab426e376e210d6741c0f700568e54 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 16 Nov 2025 10:04:57 -0500 Subject: [PATCH 5/6] add confval --- docs/source/config_file.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 7abd1f02db68..970fcd27a235 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -635,6 +635,15 @@ Configuring warnings For more information, see the :ref:`Configuring warnings ` section of the command line docs. +.. confval:: warn_redundant_annotation + + :type: boolean + :default: False + + Warns when the annotation type is the same as the inferred type. + + This option may only be set in the global section (``[mypy]``). + .. confval:: warn_redundant_casts :type: boolean From ebf40a5b05ef15bb29d2ccd6c371d5e815fc6180 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 16 Nov 2025 10:11:36 -0500 Subject: [PATCH 6/6] additional tests --- test-data/unit/check-warnings.test | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-warnings.test b/test-data/unit/check-warnings.test index f45e20a7b04d..43f63b7b378a 100644 --- a/test-data/unit/check-warnings.test +++ b/test-data/unit/check-warnings.test @@ -73,10 +73,14 @@ def f(q: Union[x, y, z]) -> None: [case testRedundantAnnotation] # flags: --warn-redundant-annotation +from typing import Literal a = 1 b: int = a +c: Literal[1] = 1 +d: list[str] = [] [out] -main:3: error: Annotation "int" is redundant (inferred type is the same) +main:4: error: Annotation "int" is redundant (inferred type is the same) +main:5: error: Annotation "Literal[1]" is redundant (inferred type is the same) [case testRedundantAnnotationClassVar] # flags: --warn-redundant-annotation @@ -88,6 +92,18 @@ class a: [out] main:5: error: Annotation "int" is redundant (inferred type is the same) +[case testRedundantAnnotationTypeVar] +# flags: --warn-redundant-annotation +from typing import Literal, TypeVar + +T = TypeVar("T") + +def f(x: T) -> T: + return x + +x: Literal[1] = f(1) +y: list[str] = f([]) + [case testRedundantAnnotationSkips] # flags: --warn-redundant-annotation from dataclasses import dataclass