Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add redundant-annotation warning
Relates to #18540
  • Loading branch information
grayjk committed Nov 14, 2025
commit 5910e0de8a197c9f6b9ab402c321c0673d5061ba
5 changes: 5 additions & 0 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 --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]
Expand Down
28 changes: 28 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
7 changes: 7 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}",
Expand Down
3 changes: 3 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions test-data/unit/check-warnings.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you also test:

  • x: Literal[1] = 1
  • y: list[str] = []
  • this:
from typing import TypeVar

def f(x: T) -> T:
  return x

x: Literal[1] = f(1)
y: list[str] = f([])

And uh, I guess it would be nice to test a case where running type checking without type context would error. (you may need to silence errors for a specific run?)... unfortunately I cannot think of any examples.

Copy link
Contributor Author

@grayjk grayjk Nov 16, 2025

Choose a reason for hiding this comment

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

And this feature would mean we can't.

If that redefinition algorithm is implemented, couldn't this flag be mutually exclusive with the redefinition flag?

Could you also test:

Tests added (commit ebf40a5). In the TypeVar test, it looks like is_same_type does not handle this. May be related to #19761

test a case where running type checking without type context would error

Does that mean a test like:

# mypy: check-untyped-defs
def f():
    return 4

def g():
    j: int = f() 

Copy link
Collaborator

Choose a reason for hiding this comment

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

If that redefinition algorithm is implemented, couldn't this flag be mutually exclusive with the redefinition flag?

Yeah, potentially. I don't think that would be good UX...

Copy link
Collaborator

@A5rocks A5rocks Nov 18, 2025

Choose a reason for hiding this comment

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

Does that mean a test like:

No, there's been issues like #20013 where part of the issue is that we're running type inference twice (once without type context for heuristics reasons, and once with) and the one without type context is erroring.

I can't think of any small reproducer because it would be a bug in mypy I think...

-- Unused 'type: ignore' comments
-- ------------------------------

Expand Down