Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
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
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 @@ -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
Expand Down Expand Up @@ -3206,6 +3207,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 +3261,31 @@ 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 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
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.type)

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
33 changes: 33 additions & 0 deletions test-data/unit/check-warnings.test
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,39 @@ 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 testRedundantAnnotationClassVar]
# flags: --warn-redundant-annotation
from typing import ClassVar

class a:
b: ClassVar[int] = 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

@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
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