From dec6452caa959f6f55418063de869cb1f4bff887 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Sun, 16 Nov 2025 19:09:33 -0300 Subject: [PATCH 01/16] Add type annotations checker Signed-off-by: Alvaro Frias --- pylint/checkers/type_annotations.py | 181 ++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 pylint/checkers/type_annotations.py diff --git a/pylint/checkers/type_annotations.py b/pylint/checkers/type_annotations.py new file mode 100644 index 0000000000..cae8158376 --- /dev/null +++ b/pylint/checkers/type_annotations.py @@ -0,0 +1,181 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Checker for type annotations in function definitions.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from astroid import nodes + +from pylint import checkers +from pylint.checkers import utils + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class TypeAnnotationChecker(checkers.BaseChecker): + """Checker for enforcing type annotations on functions and methods. + + This checker verifies that functions and methods have appropriate + type annotations for return values and parameters. + """ + + name = "type-annotation" + msgs = { + "C2901": ( + "Missing return type annotation for function %r", + "missing-return-type-annotation", + "Used when a function or method does not have a return type annotation. " + "Type annotations improve code readability and help with static type checking.", + ), + "C2902": ( + "Missing type annotation for parameter %r in function %r", + "missing-param-type-annotation", + "Used when a function or method parameter does not have a type annotation. " + "Type annotations improve code readability and help with static type checking.", + ), + } + + @utils.only_required_for_messages( + "missing-return-type-annotation", "missing-param-type-annotation" + ) + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + """Check for missing type annotations in regular functions.""" + self._check_return_type_annotation(node) + self._check_param_type_annotations(node) + + @utils.only_required_for_messages( + "missing-return-type-annotation", "missing-param-type-annotation" + ) + def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None: + """Check for missing type annotations in async functions.""" + self._check_return_type_annotation(node) + self._check_param_type_annotations(node) + + def _check_return_type_annotation( + self, node: nodes.FunctionDef | nodes.AsyncFunctionDef + ) -> None: + """Check if a function has a return type annotation. + + Args: + node: The function definition node to check + """ + # Skip if function already has return type annotation + if node.returns is not None: + return + + # Skip if function has type comment with return type + if node.type_comment_returns: + return + + # Skip __init__ methods as they implicitly return None + if node.name == "__init__": + return + + # Skip abstract methods (often overridden with proper annotations) + if utils.decorated_with(node, ["abc.abstractmethod", "abc.abstractproperty"]): + return + + # Skip overload decorators (stub definitions) + if utils.decorated_with( + node, ["typing.overload", "typing_extensions.overload"] + ): + return + + # Skip property setters and deleters (return value not meaningful) + if utils.decorated_with( + node, ["property", "*.setter", "*.deleter", "builtins.property"] + ): + return + + # Emit the message + self.add_message("missing-return-type-annotation", node=node, args=(node.name,)) + + def _check_param_type_annotations( + self, node: nodes.FunctionDef | nodes.AsyncFunctionDef + ) -> None: + """Check if function parameters have type annotations. + + Args: + node: The function definition node to check + """ + # Skip abstract methods + if utils.decorated_with(node, ["abc.abstractmethod", "abc.abstractproperty"]): + return + + # Skip overload decorators + if utils.decorated_with( + node, ["typing.overload", "typing_extensions.overload"] + ): + return + + arguments = node.args + + # Check positional-only args + if arguments.posonlyargs: + annotations = arguments.posonlyargs_annotations or [] + for idx, arg in enumerate(arguments.posonlyargs): + if arg.name in {"self", "cls"}: + continue + if idx >= len(annotations) or annotations[idx] is None: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arg.name, node.name), + ) + + # Check regular args (skip self/cls for methods) + if arguments.args: + annotations = arguments.annotations or [] + start_idx = 0 + # Skip 'self' or 'cls' for methods + if ( + arguments.args + and arguments.args[0].name in {"self", "cls"} + and isinstance(node.parent, nodes.ClassDef) + ): + start_idx = 1 + + for idx, arg in enumerate(arguments.args[start_idx:], start=start_idx): + if idx >= len(annotations) or annotations[idx] is None: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arg.name, node.name), + ) + + # Check *args + if arguments.vararg and not arguments.varargannotation: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arguments.vararg, node.name), + ) + + # Check keyword-only args + if arguments.kwonlyargs: + annotations = arguments.kwonlyargs_annotations or [] + for idx, arg in enumerate(arguments.kwonlyargs): + if idx >= len(annotations) or annotations[idx] is None: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arg.name, node.name), + ) + + # Check **kwargs + if arguments.kwarg and not arguments.kwargannotation: + self.add_message( + "missing-param-type-annotation", + node=node, + args=(arguments.kwarg, node.name), + ) + + +def register(linter: PyLinter) -> None: + """Register the checker with the linter.""" + linter.register_checker(TypeAnnotationChecker(linter)) From 967978e69e9fcb5d28f0041fd1e2546ddd976f93 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Sun, 16 Nov 2025 19:09:47 -0300 Subject: [PATCH 02/16] Add tests Signed-off-by: Alvaro Frias --- tests/checkers/unittest_type_annotations.py | 271 ++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 tests/checkers/unittest_type_annotations.py diff --git a/tests/checkers/unittest_type_annotations.py b/tests/checkers/unittest_type_annotations.py new file mode 100644 index 0000000000..a19f72c3ed --- /dev/null +++ b/tests/checkers/unittest_type_annotations.py @@ -0,0 +1,271 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt + +"""Tests for the type_annotations checker.""" + +from __future__ import annotations + +import astroid + +from pylint.checkers.type_annotations import TypeAnnotationChecker +from pylint.testutils import CheckerTestCase, MessageTest + + +class TestTypeAnnotationChecker(CheckerTestCase): + """Tests for TypeAnnotationChecker.""" + + CHECKER_CLASS = TypeAnnotationChecker + + def test_missing_return_type_annotation(self) -> None: + """Test detection of missing return type annotation.""" + node = astroid.extract_node( + """ + def foo(x): #@ + return x + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-return-type-annotation", + args=("foo",), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + ): + self.checker.visit_functiondef(node) + + def test_function_with_return_type_annotation(self) -> None: + """Test that functions with return type annotations don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(x: int) -> int: #@ + return x + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_init_method_skipped(self) -> None: + """Test that __init__ methods are skipped for return type.""" + node = astroid.extract_node( + """ + class MyClass: + def __init__(self, x): #@ + self.x = x + """ + ) + # __init__ should skip return type check, but still check params + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "__init__"), + node=node, + line=3, + col_offset=4, + end_line=3, + end_col_offset=16, + ) + ): + self.checker.visit_functiondef(node) + + def test_async_function_missing_return_type(self) -> None: + """Test detection in async functions.""" + node = astroid.extract_node( + """ + async def foo(x): #@ + return x + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-return-type-annotation", + args=("foo",), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=13, + ), + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=13, + ), + ): + self.checker.visit_asyncfunctiondef(node) + + def test_missing_param_type_annotation(self) -> None: + """Test detection of missing parameter type annotation.""" + node = astroid.extract_node( + """ + def foo(x) -> int: #@ + return x + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_function_with_all_annotations(self) -> None: + """Test that fully annotated functions don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(x: int, y: str) -> bool: #@ + return True + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_method_self_parameter_skipped(self) -> None: + """Test that 'self' parameter is skipped in methods.""" + node = astroid.extract_node( + """ + class MyClass: + def foo(self, x: int) -> int: #@ + return x + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_classmethod_cls_parameter_skipped(self) -> None: + """Test that 'cls' parameter is skipped in classmethods.""" + node = astroid.extract_node( + """ + class MyClass: + @classmethod + def foo(cls, x: int) -> int: #@ + return x + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_abstract_method_skipped(self) -> None: + """Test that abstract methods are skipped.""" + node = astroid.extract_node( + """ + from abc import abstractmethod + + class MyClass: + @abstractmethod + def foo(self, x): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_property_skipped(self) -> None: + """Test that property methods are skipped.""" + node = astroid.extract_node( + """ + class MyClass: + @property + def foo(self): #@ + return 42 + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_vararg_missing_annotation(self) -> None: + """Test detection of missing *args annotation.""" + node = astroid.extract_node( + """ + def foo(*args) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("args", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_kwarg_missing_annotation(self) -> None: + """Test detection of missing **kwargs annotation.""" + node = astroid.extract_node( + """ + def foo(**kwargs) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("kwargs", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_fully_annotated_with_varargs(self) -> None: + """Test that fully annotated functions with *args and **kwargs work.""" + node = astroid.extract_node( + """ + def foo(x: int, *args: str, **kwargs: bool) -> None: #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_keyword_only_args_missing_annotation(self) -> None: + """Test detection of missing keyword-only argument annotations.""" + node = astroid.extract_node( + """ + def foo(x: int, *, y) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("y", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) From 9ec24bbfe77e453ee80433798714a2e06613ace2 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Sun, 16 Nov 2025 19:29:52 -0300 Subject: [PATCH 03/16] Add docs Signed-off-by: Alvaro Frias --- .../m/missing-param-type-annotation/bad.py | 10 ++++++++++ .../missing-param-type-annotation/details.rst | 20 +++++++++++++++++++ .../m/missing-param-type-annotation/good.py | 15 ++++++++++++++ .../m/missing-return-type-annotation/bad.py | 6 ++++++ .../details.rst | 13 ++++++++++++ .../m/missing-return-type-annotation/good.py | 11 ++++++++++ doc/whatsnew/fragments/3853.new_check | 5 +++++ 7 files changed, 80 insertions(+) create mode 100644 doc/data/messages/m/missing-param-type-annotation/bad.py create mode 100644 doc/data/messages/m/missing-param-type-annotation/details.rst create mode 100644 doc/data/messages/m/missing-param-type-annotation/good.py create mode 100644 doc/data/messages/m/missing-return-type-annotation/bad.py create mode 100644 doc/data/messages/m/missing-return-type-annotation/details.rst create mode 100644 doc/data/messages/m/missing-return-type-annotation/good.py create mode 100644 doc/whatsnew/fragments/3853.new_check diff --git a/doc/data/messages/m/missing-param-type-annotation/bad.py b/doc/data/messages/m/missing-param-type-annotation/bad.py new file mode 100644 index 0000000000..a12423d7dc --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/bad.py @@ -0,0 +1,10 @@ +def greet(name): # [missing-param-type-annotation] + return f"Hello, {name}!" + + +def add(x, y) -> int: # [missing-param-type-annotation] + return x + y + + +def process(*args, **kwargs): # [missing-param-type-annotation] + return combine(args, kwargs) diff --git a/doc/data/messages/m/missing-param-type-annotation/details.rst b/doc/data/messages/m/missing-param-type-annotation/details.rst new file mode 100644 index 0000000000..baa72cab0a --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/details.rst @@ -0,0 +1,20 @@ +Type annotations improve code readability and enable better static analysis. This check +ensures that all function and method parameters have type annotations, making the expected +types clear and allowing type checkers like mypy to verify correct usage. + +This check is opt-in (disabled by default) to maintain backward compatibility. Enable it +with ``--enable=missing-param-type-annotation``. + +The check automatically skips: + +- ``self`` and ``cls`` parameters in methods +- Parameters in abstract methods (``@abstractmethod``, ``@abstractproperty``) +- Parameters in overload stub definitions (``@typing.overload``) + +All parameter types are checked, including: + +- Regular positional parameters +- Positional-only parameters (before ``/``) +- Keyword-only parameters (after ``*``) +- Variadic positional parameters (``*args``) +- Variadic keyword parameters (``**kwargs``) diff --git a/doc/data/messages/m/missing-param-type-annotation/good.py b/doc/data/messages/m/missing-param-type-annotation/good.py new file mode 100644 index 0000000000..e41e3abbaf --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/good.py @@ -0,0 +1,15 @@ +def greet(name: str) -> str: + return f"Hello, {name}!" + + +def add(x: int, y: int) -> int: + return x + y + + +def process(*args: str, **kwargs: bool) -> dict: + return combine(args, kwargs) + + +class Calculator: + def compute(self, x: int, y: int) -> int: # self doesn't need annotation + return x + y diff --git a/doc/data/messages/m/missing-return-type-annotation/bad.py b/doc/data/messages/m/missing-return-type-annotation/bad.py new file mode 100644 index 0000000000..5dfe959191 --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/bad.py @@ -0,0 +1,6 @@ +def calculate_sum(numbers): # [missing-return-type-annotation] + return sum(numbers) + + +async def fetch_data(url): # [missing-return-type-annotation] + return await get(url) diff --git a/doc/data/messages/m/missing-return-type-annotation/details.rst b/doc/data/messages/m/missing-return-type-annotation/details.rst new file mode 100644 index 0000000000..a6124b11a0 --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/details.rst @@ -0,0 +1,13 @@ +Type annotations improve code readability and enable better static analysis. This check +ensures that all functions and methods have return type annotations, making the code's +intent clearer and allowing type checkers like mypy to verify correctness. + +This check is opt-in (disabled by default) to maintain backward compatibility. Enable it +with ``--enable=missing-return-type-annotation``. + +The check automatically skips: + +- ``__init__`` methods (which implicitly return None) +- Abstract methods (``@abstractmethod``, ``@abstractproperty``) +- Properties and their setters/deleters +- Overload stub definitions (``@typing.overload``) diff --git a/doc/data/messages/m/missing-return-type-annotation/good.py b/doc/data/messages/m/missing-return-type-annotation/good.py new file mode 100644 index 0000000000..740c8e7e58 --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/good.py @@ -0,0 +1,11 @@ +def calculate_sum(numbers: list[int]) -> int: + return sum(numbers) + + +async def fetch_data(url: str) -> dict: + return await get(url) + + +class Calculator: + def __init__(self, initial: int): # __init__ doesn't need return type + self.value = initial diff --git a/doc/whatsnew/fragments/3853.new_check b/doc/whatsnew/fragments/3853.new_check new file mode 100644 index 0000000000..5d5bfab207 --- /dev/null +++ b/doc/whatsnew/fragments/3853.new_check @@ -0,0 +1,5 @@ +Add ``missing-return-type-annotation`` and ``missing-param-type-annotation`` checks to enforce type annotation presence in functions and methods. + +These new convention-level checks help teams enforce type annotation standards. Both checks are opt-in (disabled by default) and can be enabled independently for granular control. The checks intelligently skip ``self``/``cls`` parameters, ``__init__`` methods (return type only), and methods decorated with ``@abstractmethod``, ``@property``, or ``@typing.overload``. + +Closes #3853 From 75252e6a4f37b00a40557fd5718411330fd110d7 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Sun, 16 Nov 2025 19:33:35 -0300 Subject: [PATCH 04/16] pylint Signed-off-by: Alvaro Frias --- pylint/checkers/type_annotations.py | 2 +- tests/checkers/unittest_type_annotations.py | 2 +- tests/message/conftest.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pylint/checkers/type_annotations.py b/pylint/checkers/type_annotations.py index cae8158376..82810e4c47 100644 --- a/pylint/checkers/type_annotations.py +++ b/pylint/checkers/type_annotations.py @@ -86,7 +86,7 @@ def _check_return_type_annotation( ): return - # Skip property setters and deleters (return value not meaningful) + # Skip property setters and delete methods (return value not meaningful) if utils.decorated_with( node, ["property", "*.setter", "*.deleter", "builtins.property"] ): diff --git a/tests/checkers/unittest_type_annotations.py b/tests/checkers/unittest_type_annotations.py index a19f72c3ed..97aff9233d 100644 --- a/tests/checkers/unittest_type_annotations.py +++ b/tests/checkers/unittest_type_annotations.py @@ -156,7 +156,7 @@ def foo(self, x: int) -> int: #@ self.checker.visit_functiondef(node) def test_classmethod_cls_parameter_skipped(self) -> None: - """Test that 'cls' parameter is skipped in classmethods.""" + """Test that 'cls' parameter is skipped in class methods.""" node = astroid.extract_node( """ class MyClass: diff --git a/tests/message/conftest.py b/tests/message/conftest.py index 57567f4385..b56dade6b9 100644 --- a/tests/message/conftest.py +++ b/tests/message/conftest.py @@ -16,12 +16,12 @@ @pytest.fixture -def msgid(): +def msgid() -> str: return "W1234" @pytest.fixture -def symbol(): +def symbol() -> str: return "msg-symbol" From 98a3b522f9b407f2e96fae81f5576043dd264eb1 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Sun, 16 Nov 2025 19:40:52 -0300 Subject: [PATCH 05/16] fix id's clash Signed-off-by: Alvaro Frias --- pylint/checkers/type_annotations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/type_annotations.py b/pylint/checkers/type_annotations.py index 82810e4c47..e0407c3289 100644 --- a/pylint/checkers/type_annotations.py +++ b/pylint/checkers/type_annotations.py @@ -26,13 +26,13 @@ class TypeAnnotationChecker(checkers.BaseChecker): name = "type-annotation" msgs = { - "C2901": ( + "C3801": ( "Missing return type annotation for function %r", "missing-return-type-annotation", "Used when a function or method does not have a return type annotation. " "Type annotations improve code readability and help with static type checking.", ), - "C2902": ( + "C3802": ( "Missing type annotation for parameter %r in function %r", "missing-param-type-annotation", "Used when a function or method parameter does not have a type annotation. " From a4e83616412c21b7e210ab77811f16b22b8a3874 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Sun, 16 Nov 2025 19:51:32 -0300 Subject: [PATCH 06/16] update docs Signed-off-by: Alvaro Frias --- doc/data/messages/m/missing-param-type-annotation/bad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/data/messages/m/missing-param-type-annotation/bad.py b/doc/data/messages/m/missing-param-type-annotation/bad.py index a12423d7dc..4df996eb32 100644 --- a/doc/data/messages/m/missing-param-type-annotation/bad.py +++ b/doc/data/messages/m/missing-param-type-annotation/bad.py @@ -2,9 +2,9 @@ def greet(name): # [missing-param-type-annotation] return f"Hello, {name}!" -def add(x, y) -> int: # [missing-param-type-annotation] +def add(x, y) -> int: # [missing-param-type-annotation, missing-param-type-annotation] return x + y -def process(*args, **kwargs): # [missing-param-type-annotation] +def process(*args, **kwargs): # [missing-param-type-annotation, missing-param-type-annotation] return combine(args, kwargs) From 74eb01340229bedce3f2a56cfe4e2ffa41729a9b Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Mon, 24 Nov 2025 22:10:56 -0300 Subject: [PATCH 07/16] corrections Signed-off-by: Alvaro Frias --- .../m/missing-param-type-annotation/bad.py | 4 ++-- .../m/missing-return-type-annotation/bad.py | 4 ++-- pylint/checkers/type_annotations.py | 20 +++---------------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/doc/data/messages/m/missing-param-type-annotation/bad.py b/doc/data/messages/m/missing-param-type-annotation/bad.py index 4df996eb32..97c8e98918 100644 --- a/doc/data/messages/m/missing-param-type-annotation/bad.py +++ b/doc/data/messages/m/missing-param-type-annotation/bad.py @@ -1,4 +1,4 @@ -def greet(name): # [missing-param-type-annotation] +def greet(name) -> str: # [missing-param-type-annotation] return f"Hello, {name}!" @@ -6,5 +6,5 @@ def add(x, y) -> int: # [missing-param-type-annotation, missing-param-type-anno return x + y -def process(*args, **kwargs): # [missing-param-type-annotation, missing-param-type-annotation] +def process(*args, **kwargs) -> dict: # [missing-param-type-annotation, missing-param-type-annotation] return combine(args, kwargs) diff --git a/doc/data/messages/m/missing-return-type-annotation/bad.py b/doc/data/messages/m/missing-return-type-annotation/bad.py index 5dfe959191..f7fb9e789e 100644 --- a/doc/data/messages/m/missing-return-type-annotation/bad.py +++ b/doc/data/messages/m/missing-return-type-annotation/bad.py @@ -1,6 +1,6 @@ -def calculate_sum(numbers): # [missing-return-type-annotation] +def calculate_sum(numbers: list[int]): # [missing-return-type-annotation] return sum(numbers) -async def fetch_data(url): # [missing-return-type-annotation] +async def fetch_data(url: str): # [missing-return-type-annotation] return await get(url) diff --git a/pylint/checkers/type_annotations.py b/pylint/checkers/type_annotations.py index e0407c3289..a9732356ce 100644 --- a/pylint/checkers/type_annotations.py +++ b/pylint/checkers/type_annotations.py @@ -31,12 +31,14 @@ class TypeAnnotationChecker(checkers.BaseChecker): "missing-return-type-annotation", "Used when a function or method does not have a return type annotation. " "Type annotations improve code readability and help with static type checking.", + {"default_enabled": False}, ), "C3802": ( "Missing type annotation for parameter %r in function %r", "missing-param-type-annotation", "Used when a function or method parameter does not have a type annotation. " "Type annotations improve code readability and help with static type checking.", + {"default_enabled": False}, ), } @@ -48,13 +50,7 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: self._check_return_type_annotation(node) self._check_param_type_annotations(node) - @utils.only_required_for_messages( - "missing-return-type-annotation", "missing-param-type-annotation" - ) - def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None: - """Check for missing type annotations in async functions.""" - self._check_return_type_annotation(node) - self._check_param_type_annotations(node) + visit_asyncfunctiondef = visit_functiondef def _check_return_type_annotation( self, node: nodes.FunctionDef | nodes.AsyncFunctionDef @@ -64,35 +60,28 @@ def _check_return_type_annotation( Args: node: The function definition node to check """ - # Skip if function already has return type annotation if node.returns is not None: return - # Skip if function has type comment with return type if node.type_comment_returns: return - # Skip __init__ methods as they implicitly return None if node.name == "__init__": return - # Skip abstract methods (often overridden with proper annotations) if utils.decorated_with(node, ["abc.abstractmethod", "abc.abstractproperty"]): return - # Skip overload decorators (stub definitions) if utils.decorated_with( node, ["typing.overload", "typing_extensions.overload"] ): return - # Skip property setters and delete methods (return value not meaningful) if utils.decorated_with( node, ["property", "*.setter", "*.deleter", "builtins.property"] ): return - # Emit the message self.add_message("missing-return-type-annotation", node=node, args=(node.name,)) def _check_param_type_annotations( @@ -103,11 +92,9 @@ def _check_param_type_annotations( Args: node: The function definition node to check """ - # Skip abstract methods if utils.decorated_with(node, ["abc.abstractmethod", "abc.abstractproperty"]): return - # Skip overload decorators if utils.decorated_with( node, ["typing.overload", "typing_extensions.overload"] ): @@ -132,7 +119,6 @@ def _check_param_type_annotations( if arguments.args: annotations = arguments.annotations or [] start_idx = 0 - # Skip 'self' or 'cls' for methods if ( arguments.args and arguments.args[0].name in {"self", "cls"} From 1ce8b62dac16faabcd2912196741aa657f11023e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 01:14:36 +0000 Subject: [PATCH 08/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/data/messages/m/missing-param-type-annotation/bad.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/data/messages/m/missing-param-type-annotation/bad.py b/doc/data/messages/m/missing-param-type-annotation/bad.py index 97c8e98918..84daef2a0f 100644 --- a/doc/data/messages/m/missing-param-type-annotation/bad.py +++ b/doc/data/messages/m/missing-param-type-annotation/bad.py @@ -6,5 +6,7 @@ def add(x, y) -> int: # [missing-param-type-annotation, missing-param-type-anno return x + y -def process(*args, **kwargs) -> dict: # [missing-param-type-annotation, missing-param-type-annotation] +def process( + *args, **kwargs +) -> dict: # [missing-param-type-annotation, missing-param-type-annotation] return combine(args, kwargs) From 249aace4aba58fe1c23e951ccb37276bd28e7925 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Mon, 24 Nov 2025 22:20:00 -0300 Subject: [PATCH 09/16] fix doc issue Signed-off-by: Alvaro Frias --- doc/data/messages/m/missing-param-type-annotation/bad.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/data/messages/m/missing-param-type-annotation/bad.py b/doc/data/messages/m/missing-param-type-annotation/bad.py index 97c8e98918..344cb05772 100644 --- a/doc/data/messages/m/missing-param-type-annotation/bad.py +++ b/doc/data/messages/m/missing-param-type-annotation/bad.py @@ -6,5 +6,7 @@ def add(x, y) -> int: # [missing-param-type-annotation, missing-param-type-anno return x + y -def process(*args, **kwargs) -> dict: # [missing-param-type-annotation, missing-param-type-annotation] +def process( # [missing-param-type-annotation, missing-param-type-annotation] + *args, **kwargs +) -> dict: return combine(args, kwargs) From c4e156ae58201b750c23e831eb4c6152973c9dff Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Tue, 25 Nov 2025 22:37:12 -0300 Subject: [PATCH 10/16] fix tests Signed-off-by: Alvaro Frias --- pylint/testutils/testing_pylintrc | 2 ++ tests/test_self.py | 51 ++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/pylint/testutils/testing_pylintrc b/pylint/testutils/testing_pylintrc index 9429b858f9..b45e264add 100644 --- a/pylint/testutils/testing_pylintrc +++ b/pylint/testutils/testing_pylintrc @@ -7,6 +7,8 @@ disable= suppressed-message, locally-disabled, useless-suppression, + missing-param-type-annotation, + missing-return-type-annotation, enable= deprecated-pragma, diff --git a/tests/test_self.py b/tests/test_self.py index 99b50fcfd3..26c06ef749 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -270,7 +270,7 @@ def test_no_out_encoding(self) -> None: strio = StringIO() assert strio.encoding is None self._runtest( - [join(HERE, "regrtest_data", "no_stdout_encoding.py"), "--enable=all"], + [join(HERE, "regrtest_data", "no_stdout_encoding.py"), "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all"], out=strio, code=28, ) @@ -311,7 +311,7 @@ def test_enable_all_works(self) -> None: """ ) self._test_output( - [module, "--disable=I", "--enable=all", "-rn"], expected_output=expected + [module, "--disable=I,missing-param-type-annotation,missing-return-type-annotation", "--enable=all", "-rn"], expected_output=expected ) def test_wrong_import_position_when_others_disabled(self) -> None: @@ -732,6 +732,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "-10", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -741,6 +742,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "6", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -750,6 +752,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "7.5", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -759,6 +762,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "7.6", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -769,6 +773,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "-11", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_minus10.py"), ], @@ -778,6 +783,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "-10", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_minus10.py"), ], @@ -789,6 +795,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "-9", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", "--evaluation", "0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)", @@ -800,6 +807,7 @@ def test_fail_under(self) -> None: [ "--fail-under", "-5", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", "--evaluation", "0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)", @@ -846,6 +854,7 @@ def test_fail_on(self, fu_score: int, fo_msgs: str, fname: str, out: int) -> Non "--fail-under", f"{fu_score:f}", f"--fail-on={fo_msgs}", + "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", fname), # Use the old form of the evaluation that can go negative @@ -1156,7 +1165,7 @@ def test_one_module_fatal_error(self) -> None: ) def test_fail_on_info_only_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on_info_only.py") - self._runtest([path, *args], code=expected) + self._runtest([path, *args, "--disable=missing-param-type-annotation,missing-return-type-annotation"], code=expected) @pytest.mark.parametrize( "output_format, expected_output", @@ -1421,7 +1430,7 @@ def test_line_too_long_useless_suppression(self) -> None: """ ) - self._test_output([module, "--enable=all"], expected_output=expected) + self._test_output([module, "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all"], expected_output=expected) def test_output_no_header(self) -> None: module = join(HERE, "data", "clientmodule_test.py") @@ -1445,6 +1454,40 @@ def test_no_name_in_module(self) -> None: [module, "-E"], expected_output="", unexpected_output=unexpected ) + def test_type_annotation_checker(self) -> None: + """Test that the type annotation checker works correctly when enabled.""" + module = join(HERE, "regrtest_data", "type_annotations_test.py") + expected = textwrap.dedent( + f""" + ************* Module type_annotations_test + {module}:4:0: C3801: Missing return type annotation for function 'missing_return_type' (missing-return-type-annotation) + {module}:9:0: C3802: Missing type annotation for parameter 'x' in function 'missing_param_types' (missing-param-type-annotation) + {module}:9:0: C3802: Missing type annotation for parameter 'y' in function 'missing_param_types' (missing-param-type-annotation) + {module}:14:0: C3801: Missing return type annotation for function 'missing_all_annotations' (missing-return-type-annotation) + {module}:14:0: C3802: Missing type annotation for parameter 'x' in function 'missing_all_annotations' (missing-param-type-annotation) + {module}:14:0: C3802: Missing type annotation for parameter 'y' in function 'missing_all_annotations' (missing-param-type-annotation) + {module}:31:4: C3801: Missing return type annotation for function 'get_value' (missing-return-type-annotation) + {module}:35:4: C3801: Missing return type annotation for function 'set_value' (missing-return-type-annotation) + {module}:35:4: C3802: Missing type annotation for parameter 'value' in function 'set_value' (missing-param-type-annotation) + {module}:44:0: C3801: Missing return type annotation for function 'async_missing_return' (missing-return-type-annotation) + """ + ) + # Test with the checker explicitly enabled + self._test_output( + [module, "--enable=missing-return-type-annotation,missing-param-type-annotation", "-rn"], + expected_output=expected, + ) + + def test_type_annotation_checker_disabled_by_default(self) -> None: + """Test that the type annotation checker is disabled by default.""" + module = join(HERE, "regrtest_data", "type_annotations_test.py") + # Without explicitly enabling the checker, no type annotation messages should appear + out = StringIO() + self._runtest([module], out=out, code=0) + output = out.getvalue() + assert "missing-return-type-annotation" not in output + assert "missing-param-type-annotation" not in output + class TestCallbackOptions: """Test for all callback options we support.""" From 54e81fe5547dc3a5f804bbb0e71ddd665d3177ca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 01:38:13 +0000 Subject: [PATCH 11/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_self.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/test_self.py b/tests/test_self.py index 26c06ef749..4e1512f2a0 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -270,7 +270,11 @@ def test_no_out_encoding(self) -> None: strio = StringIO() assert strio.encoding is None self._runtest( - [join(HERE, "regrtest_data", "no_stdout_encoding.py"), "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all"], + [ + join(HERE, "regrtest_data", "no_stdout_encoding.py"), + "--disable=missing-param-type-annotation,missing-return-type-annotation", + "--enable=all", + ], out=strio, code=28, ) @@ -311,7 +315,13 @@ def test_enable_all_works(self) -> None: """ ) self._test_output( - [module, "--disable=I,missing-param-type-annotation,missing-return-type-annotation", "--enable=all", "-rn"], expected_output=expected + [ + module, + "--disable=I,missing-param-type-annotation,missing-return-type-annotation", + "--enable=all", + "-rn", + ], + expected_output=expected, ) def test_wrong_import_position_when_others_disabled(self) -> None: @@ -1165,7 +1175,14 @@ def test_one_module_fatal_error(self) -> None: ) def test_fail_on_info_only_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on_info_only.py") - self._runtest([path, *args, "--disable=missing-param-type-annotation,missing-return-type-annotation"], code=expected) + self._runtest( + [ + path, + *args, + "--disable=missing-param-type-annotation,missing-return-type-annotation", + ], + code=expected, + ) @pytest.mark.parametrize( "output_format, expected_output", @@ -1430,7 +1447,14 @@ def test_line_too_long_useless_suppression(self) -> None: """ ) - self._test_output([module, "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all"], expected_output=expected) + self._test_output( + [ + module, + "--disable=missing-param-type-annotation,missing-return-type-annotation", + "--enable=all", + ], + expected_output=expected, + ) def test_output_no_header(self) -> None: module = join(HERE, "data", "clientmodule_test.py") @@ -1474,7 +1498,11 @@ def test_type_annotation_checker(self) -> None: ) # Test with the checker explicitly enabled self._test_output( - [module, "--enable=missing-return-type-annotation,missing-param-type-annotation", "-rn"], + [ + module, + "--enable=missing-return-type-annotation,missing-param-type-annotation", + "-rn", + ], expected_output=expected, ) From 34606a800ecc6ac3e41b0fb0ab3651feb7db6b23 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Tue, 25 Nov 2025 23:00:11 -0300 Subject: [PATCH 12/16] fix tests Signed-off-by: Alvaro Frias --- tests/regrtest_data/type_annotations.py | 51 +++++++++++++++++ tests/test_self.py | 74 +++++++++++++++++++------ 2 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 tests/regrtest_data/type_annotations.py diff --git a/tests/regrtest_data/type_annotations.py b/tests/regrtest_data/type_annotations.py new file mode 100644 index 0000000000..1197541cd3 --- /dev/null +++ b/tests/regrtest_data/type_annotations.py @@ -0,0 +1,51 @@ +"""Test file for type annotation checker.""" + + +def missing_return_type(x: int, y: int): # Missing return type + """Function missing return type annotation.""" + return x + y + + +def missing_param_types(x, y) -> int: # Missing parameter types + """Function missing parameter type annotations.""" + return x + y + + +def missing_all_annotations(x, y): # Missing both + """Function missing all type annotations.""" + return x + y + + +def fully_annotated(x: int, y: int) -> int: # OK - fully annotated + """Function with complete type annotations.""" + return x + y + + +class TestClass: + """Test class for type annotations.""" + + def __init__(self, value: int): # OK - __init__ doesn't need return type + """Initialize with value.""" + self.value = value + + def get_value(self): # Missing return type + """Get the value.""" + return self.value + + def set_value(self, value): # Missing parameter type and return type + """Set the value.""" + self.value = value + + def compute(self, x: int) -> int: # OK - fully annotated + """Compute something.""" + return self.value + x + + +async def async_missing_return(x: int): # Missing return type + """Async function missing return type.""" + return x * 2 + + +async def async_fully_annotated(x: int) -> int: # OK - fully annotated + """Async function with complete annotations.""" + return x * 2 diff --git a/tests/test_self.py b/tests/test_self.py index 26c06ef749..616117edbd 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -270,7 +270,11 @@ def test_no_out_encoding(self) -> None: strio = StringIO() assert strio.encoding is None self._runtest( - [join(HERE, "regrtest_data", "no_stdout_encoding.py"), "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all"], + [ + join(HERE, "regrtest_data", "no_stdout_encoding.py"), + "--disable=missing-param-type-annotation,missing-return-type-annotation", + "--enable=all", + ], out=strio, code=28, ) @@ -311,7 +315,13 @@ def test_enable_all_works(self) -> None: """ ) self._test_output( - [module, "--disable=I,missing-param-type-annotation,missing-return-type-annotation", "--enable=all", "-rn"], expected_output=expected + [ + module, + "--disable=I,missing-param-type-annotation,missing-return-type-annotation", + "--enable=all", + "-rn", + ], + expected_output=expected, ) def test_wrong_import_position_when_others_disabled(self) -> None: @@ -1165,7 +1175,14 @@ def test_one_module_fatal_error(self) -> None: ) def test_fail_on_info_only_exit_code(self, args: list[str], expected: int) -> None: path = join(HERE, "regrtest_data", "fail_on_info_only.py") - self._runtest([path, *args, "--disable=missing-param-type-annotation,missing-return-type-annotation"], code=expected) + self._runtest( + [ + path, + *args, + "--disable=missing-param-type-annotation,missing-return-type-annotation", + ], + code=expected, + ) @pytest.mark.parametrize( "output_format, expected_output", @@ -1430,7 +1447,14 @@ def test_line_too_long_useless_suppression(self) -> None: """ ) - self._test_output([module, "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all"], expected_output=expected) + self._test_output( + [ + module, + "--disable=missing-param-type-annotation,missing-return-type-annotation", + "--enable=all", + ], + expected_output=expected, + ) def test_output_no_header(self) -> None: module = join(HERE, "data", "clientmodule_test.py") @@ -1456,31 +1480,45 @@ def test_no_name_in_module(self) -> None: def test_type_annotation_checker(self) -> None: """Test that the type annotation checker works correctly when enabled.""" - module = join(HERE, "regrtest_data", "type_annotations_test.py") + module = join(HERE, "regrtest_data", "type_annotations.py") expected = textwrap.dedent( f""" - ************* Module type_annotations_test - {module}:4:0: C3801: Missing return type annotation for function 'missing_return_type' (missing-return-type-annotation) - {module}:9:0: C3802: Missing type annotation for parameter 'x' in function 'missing_param_types' (missing-param-type-annotation) - {module}:9:0: C3802: Missing type annotation for parameter 'y' in function 'missing_param_types' (missing-param-type-annotation) - {module}:14:0: C3801: Missing return type annotation for function 'missing_all_annotations' (missing-return-type-annotation) - {module}:14:0: C3802: Missing type annotation for parameter 'x' in function 'missing_all_annotations' (missing-param-type-annotation) - {module}:14:0: C3802: Missing type annotation for parameter 'y' in function 'missing_all_annotations' (missing-param-type-annotation) - {module}:31:4: C3801: Missing return type annotation for function 'get_value' (missing-return-type-annotation) - {module}:35:4: C3801: Missing return type annotation for function 'set_value' (missing-return-type-annotation) - {module}:35:4: C3802: Missing type annotation for parameter 'value' in function 'set_value' (missing-param-type-annotation) - {module}:44:0: C3801: Missing return type annotation for function 'async_missing_return' (missing-return-type-annotation) + ************* Module type_annotations + {module}:4:0: C3801: Missing return type annotation for function """ + f"""'missing_return_type' (missing-return-type-annotation) + {module}:9:0: C3802: Missing type annotation for parameter 'x' in """ + f"""function 'missing_param_types' (missing-param-type-annotation) + {module}:9:0: C3802: Missing type annotation for parameter 'y' in """ + f"""function 'missing_param_types' (missing-param-type-annotation) + {module}:14:0: C3801: Missing return type annotation for function """ + f"""'missing_all_annotations' (missing-return-type-annotation) + {module}:14:0: C3802: Missing type annotation for parameter 'x' in """ + f"""function 'missing_all_annotations' (missing-param-type-annotation) + {module}:14:0: C3802: Missing type annotation for parameter 'y' in """ + f"""function 'missing_all_annotations' (missing-param-type-annotation) + {module}:31:4: C3801: Missing return type annotation for function """ + f"""'get_value' (missing-return-type-annotation) + {module}:35:4: C3801: Missing return type annotation for function """ + f"""'set_value' (missing-return-type-annotation) + {module}:35:4: C3802: Missing type annotation for parameter 'value' """ + f"""in function 'set_value' (missing-param-type-annotation) + {module}:44:0: C3801: Missing return type annotation for function """ + f"""'async_missing_return' (missing-return-type-annotation) """ ) # Test with the checker explicitly enabled self._test_output( - [module, "--enable=missing-return-type-annotation,missing-param-type-annotation", "-rn"], + [ + module, + "--enable=missing-return-type-annotation,missing-param-type-annotation", + "-rn", + ], expected_output=expected, ) def test_type_annotation_checker_disabled_by_default(self) -> None: """Test that the type annotation checker is disabled by default.""" - module = join(HERE, "regrtest_data", "type_annotations_test.py") + module = join(HERE, "regrtest_data", "type_annotations.py") # Without explicitly enabling the checker, no type annotation messages should appear out = StringIO() self._runtest([module], out=out, code=0) From 7df9b81d28ab8a5c17b6d42c6c6e78dbc91673a6 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Tue, 25 Nov 2025 23:26:34 -0300 Subject: [PATCH 13/16] coverage Signed-off-by: Alvaro Frias --- pylint/checkers/type_annotations.py | 10 +- tests/checkers/unittest_type_annotations.py | 259 +++++++++++++++++++- 2 files changed, 265 insertions(+), 4 deletions(-) diff --git a/pylint/checkers/type_annotations.py b/pylint/checkers/type_annotations.py index a9732356ce..ac12bc23d2 100644 --- a/pylint/checkers/type_annotations.py +++ b/pylint/checkers/type_annotations.py @@ -77,9 +77,10 @@ def _check_return_type_annotation( ): return - if utils.decorated_with( - node, ["property", "*.setter", "*.deleter", "builtins.property"] - ): + if utils.decorated_with(node, ["property", "builtins.property"]): + return + + if utils.is_property_setter_or_deleter(node): return self.add_message("missing-return-type-annotation", node=node, args=(node.name,)) @@ -100,6 +101,9 @@ def _check_param_type_annotations( ): return + if utils.is_property_setter_or_deleter(node): + return + arguments = node.args # Check positional-only args diff --git a/tests/checkers/unittest_type_annotations.py b/tests/checkers/unittest_type_annotations.py index 97aff9233d..a888da6e5c 100644 --- a/tests/checkers/unittest_type_annotations.py +++ b/tests/checkers/unittest_type_annotations.py @@ -12,7 +12,9 @@ from pylint.testutils import CheckerTestCase, MessageTest -class TestTypeAnnotationChecker(CheckerTestCase): +class TestTypeAnnotationChecker( + CheckerTestCase +): # pylint: disable=too-many-public-methods """Tests for TypeAnnotationChecker.""" CHECKER_CLASS = TypeAnnotationChecker @@ -269,3 +271,258 @@ def foo(x: int, *, y) -> None: #@ ) ): self.checker.visit_functiondef(node) + + def test_type_comment_returns_skipped(self) -> None: + """Test that functions with type comment returns are skipped.""" + node = astroid.extract_node( + """ + def foo(x): #@ + # type: (int) -> int + return x + """ + ) + # Should only check params, not return type + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_abstract_property_skipped(self) -> None: + """Test that abstract properties are skipped.""" + node = astroid.extract_node( + """ + from abc import abstractproperty + + class MyClass: + @abstractproperty + def foo(self): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_typing_overload_skipped(self) -> None: + """Test that typing.overload decorated functions are skipped.""" + node = astroid.extract_node( + """ + from typing import overload + + @overload + def foo(x): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_typing_extensions_overload_skipped(self) -> None: + """Test that typing_extensions.overload decorated functions are skipped.""" + node = astroid.extract_node( + """ + from typing_extensions import overload + + @overload + def foo(x): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_property_setter_skipped(self) -> None: + """Test that property setters are skipped.""" + node = astroid.extract_node( + """ + class MyClass: + @property + def foo(self) -> int: + return 42 + + @foo.setter + def foo(self, value): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_property_deleter_skipped(self) -> None: + """Test that property deleters are skipped.""" + node = astroid.extract_node( + """ + class MyClass: + @property + def foo(self) -> int: + return 42 + + @foo.deleter + def foo(self): #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_builtins_property_skipped(self) -> None: + """Test that builtins.property decorated functions are skipped.""" + node = astroid.extract_node( + """ + import builtins + + class MyClass: + @builtins.property + def foo(self): #@ + return 42 + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_positional_only_args_missing_annotation(self) -> None: + """Test detection of missing positional-only argument annotations.""" + node = astroid.extract_node( + """ + def foo(x, y, /) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + MessageTest( + msg_id="missing-param-type-annotation", + args=("y", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ), + ): + self.checker.visit_functiondef(node) + + def test_positional_only_args_with_self_skipped(self) -> None: + """Test that self is skipped in positional-only args.""" + node = astroid.extract_node( + """ + class MyClass: + def foo(self, x, /) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=3, + col_offset=4, + end_line=3, + end_col_offset=11, + ) + ): + self.checker.visit_functiondef(node) + + def test_positional_only_args_fully_annotated(self) -> None: + """Test that fully annotated positional-only args don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(x: int, y: str, /) -> None: #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) + + def test_positional_only_args_with_cls_skipped(self) -> None: + """Test that cls is skipped in positional-only args for classmethods.""" + node = astroid.extract_node( + """ + class MyClass: + @classmethod + def foo(cls, x, /) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=4, + col_offset=4, + end_line=4, + end_col_offset=11, + ) + ): + self.checker.visit_functiondef(node) + + def test_method_with_second_arg_missing_annotation(self) -> None: + """Test that only self/cls is skipped, not subsequent args.""" + node = astroid.extract_node( + """ + class MyClass: + def foo(self, x, y: int) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("x", "foo"), + node=node, + line=3, + col_offset=4, + end_line=3, + end_col_offset=11, + ) + ): + self.checker.visit_functiondef(node) + + def test_mixed_positional_and_regular_args(self) -> None: + """Test functions with both positional-only and regular args.""" + node = astroid.extract_node( + """ + def foo(x: int, /, y, z: str) -> None: #@ + pass + """ + ) + with self.assertAddsMessages( + MessageTest( + msg_id="missing-param-type-annotation", + args=("y", "foo"), + node=node, + line=2, + col_offset=0, + end_line=2, + end_col_offset=7, + ) + ): + self.checker.visit_functiondef(node) + + def test_keyword_only_args_fully_annotated(self) -> None: + """Test that fully annotated keyword-only args don't trigger warnings.""" + node = astroid.extract_node( + """ + def foo(*, x: int, y: str) -> None: #@ + pass + """ + ) + with self.assertNoMessages(): + self.checker.visit_functiondef(node) From ffc43d6179e82bf29cc21e52e185729b88a503ac Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Tue, 25 Nov 2025 23:32:33 -0300 Subject: [PATCH 14/16] fix Signed-off-by: Alvaro Frias --- tests/checkers/unittest_type_annotations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/checkers/unittest_type_annotations.py b/tests/checkers/unittest_type_annotations.py index a888da6e5c..475ab7942b 100644 --- a/tests/checkers/unittest_type_annotations.py +++ b/tests/checkers/unittest_type_annotations.py @@ -356,7 +356,7 @@ def foo(self, value): #@ self.checker.visit_functiondef(node) def test_property_deleter_skipped(self) -> None: - """Test that property deleters are skipped.""" + """Test that property deleter methods are skipped.""" node = astroid.extract_node( """ class MyClass: @@ -451,7 +451,7 @@ def foo(x: int, y: str, /) -> None: #@ self.checker.visit_functiondef(node) def test_positional_only_args_with_cls_skipped(self) -> None: - """Test that cls is skipped in positional-only args for classmethods.""" + """Test that cls is skipped in positional-only args for class methods.""" node = astroid.extract_node( """ class MyClass: From 327dc2b4b9e89bee65a4ad7706912db33c9da3dc Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Thu, 4 Dec 2025 23:00:07 -0300 Subject: [PATCH 15/16] make it an extension Signed-off-by: Alvaro Frias --- .../type_annotations.py | 2 -- pylint/testutils/testing_pylintrc | 2 -- .../test_type_annotations.py} | 4 ++-- tests/test_self.py | 17 +++-------------- 4 files changed, 5 insertions(+), 20 deletions(-) rename pylint/{checkers => extensions}/type_annotations.py (98%) rename tests/{checkers/unittest_type_annotations.py => extensions/test_type_annotations.py} (99%) diff --git a/pylint/checkers/type_annotations.py b/pylint/extensions/type_annotations.py similarity index 98% rename from pylint/checkers/type_annotations.py rename to pylint/extensions/type_annotations.py index ac12bc23d2..a76d351734 100644 --- a/pylint/checkers/type_annotations.py +++ b/pylint/extensions/type_annotations.py @@ -31,14 +31,12 @@ class TypeAnnotationChecker(checkers.BaseChecker): "missing-return-type-annotation", "Used when a function or method does not have a return type annotation. " "Type annotations improve code readability and help with static type checking.", - {"default_enabled": False}, ), "C3802": ( "Missing type annotation for parameter %r in function %r", "missing-param-type-annotation", "Used when a function or method parameter does not have a type annotation. " "Type annotations improve code readability and help with static type checking.", - {"default_enabled": False}, ), } diff --git a/pylint/testutils/testing_pylintrc b/pylint/testutils/testing_pylintrc index b45e264add..9429b858f9 100644 --- a/pylint/testutils/testing_pylintrc +++ b/pylint/testutils/testing_pylintrc @@ -7,8 +7,6 @@ disable= suppressed-message, locally-disabled, useless-suppression, - missing-param-type-annotation, - missing-return-type-annotation, enable= deprecated-pragma, diff --git a/tests/checkers/unittest_type_annotations.py b/tests/extensions/test_type_annotations.py similarity index 99% rename from tests/checkers/unittest_type_annotations.py rename to tests/extensions/test_type_annotations.py index 475ab7942b..375b6e05b2 100644 --- a/tests/checkers/unittest_type_annotations.py +++ b/tests/extensions/test_type_annotations.py @@ -2,13 +2,13 @@ # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt -"""Tests for the type_annotations checker.""" +"""Tests for the type_annotations extension.""" from __future__ import annotations import astroid -from pylint.checkers.type_annotations import TypeAnnotationChecker +from pylint.extensions.type_annotations import TypeAnnotationChecker from pylint.testutils import CheckerTestCase, MessageTest diff --git a/tests/test_self.py b/tests/test_self.py index 616117edbd..674a3a0f4f 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -272,7 +272,6 @@ def test_no_out_encoding(self) -> None: self._runtest( [ join(HERE, "regrtest_data", "no_stdout_encoding.py"), - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", ], out=strio, @@ -317,7 +316,7 @@ def test_enable_all_works(self) -> None: self._test_output( [ module, - "--disable=I,missing-param-type-annotation,missing-return-type-annotation", + "--disable=I", "--enable=all", "-rn", ], @@ -742,7 +741,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "-10", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -752,7 +750,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "6", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -762,7 +759,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "7.5", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -772,7 +768,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "7.6", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_plus7_5.py"), ], @@ -783,7 +778,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "-11", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_minus10.py"), ], @@ -793,7 +787,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "-10", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", "fail_under_minus10.py"), ], @@ -805,7 +798,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "-9", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", "--evaluation", "0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)", @@ -817,7 +809,6 @@ def test_fail_under(self) -> None: [ "--fail-under", "-5", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", "--evaluation", "0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)", @@ -864,7 +855,6 @@ def test_fail_on(self, fu_score: int, fo_msgs: str, fname: str, out: int) -> Non "--fail-under", f"{fu_score:f}", f"--fail-on={fo_msgs}", - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", join(HERE, "regrtest_data", fname), # Use the old form of the evaluation that can go negative @@ -1179,7 +1169,6 @@ def test_fail_on_info_only_exit_code(self, args: list[str], expected: int) -> No [ path, *args, - "--disable=missing-param-type-annotation,missing-return-type-annotation", ], code=expected, ) @@ -1450,7 +1439,6 @@ def test_line_too_long_useless_suppression(self) -> None: self._test_output( [ module, - "--disable=missing-param-type-annotation,missing-return-type-annotation", "--enable=all", ], expected_output=expected, @@ -1506,10 +1494,11 @@ def test_type_annotation_checker(self) -> None: f"""'async_missing_return' (missing-return-type-annotation) """ ) - # Test with the checker explicitly enabled + # Test with the extension loaded and checker explicitly enabled self._test_output( [ module, + "--load-plugins=pylint.extensions.type_annotations", "--enable=missing-return-type-annotation,missing-param-type-annotation", "-rn", ], From b392941de44141a3efbbf14955714d850059561a Mon Sep 17 00:00:00 2001 From: Alvaro Frias Date: Thu, 4 Dec 2025 23:17:42 -0300 Subject: [PATCH 16/16] fix docs Signed-off-by: Alvaro Frias --- doc/data/messages/m/missing-param-type-annotation/pylintrc | 6 ++++++ doc/data/messages/m/missing-return-type-annotation/pylintrc | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 doc/data/messages/m/missing-param-type-annotation/pylintrc create mode 100644 doc/data/messages/m/missing-return-type-annotation/pylintrc diff --git a/doc/data/messages/m/missing-param-type-annotation/pylintrc b/doc/data/messages/m/missing-param-type-annotation/pylintrc new file mode 100644 index 0000000000..229b3932e3 --- /dev/null +++ b/doc/data/messages/m/missing-param-type-annotation/pylintrc @@ -0,0 +1,6 @@ +[MAIN] +load-plugins = pylint.extensions.type_annotations + +[MESSAGES CONTROL] +disable = missing-return-type-annotation +enable = missing-param-type-annotation diff --git a/doc/data/messages/m/missing-return-type-annotation/pylintrc b/doc/data/messages/m/missing-return-type-annotation/pylintrc new file mode 100644 index 0000000000..9e8a126276 --- /dev/null +++ b/doc/data/messages/m/missing-return-type-annotation/pylintrc @@ -0,0 +1,6 @@ +[MAIN] +load-plugins = pylint.extensions.type_annotations + +[MESSAGES CONTROL] +disable = missing-param-type-annotation +enable = missing-return-type-annotation