Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions doc/data/messages/m/missing-param-type-annotation/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def greet(name): # [missing-param-type-annotation]
Copy link
Member

Choose a reason for hiding this comment

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

Even the bad.py should have return type, right ?

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)
20 changes: 20 additions & 0 deletions doc/data/messages/m/missing-param-type-annotation/details.rst
Original file line number Diff line number Diff line change
@@ -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``)
15 changes: 15 additions & 0 deletions doc/data/messages/m/missing-param-type-annotation/good.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions doc/data/messages/m/missing-return-type-annotation/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def calculate_sum(numbers): # [missing-return-type-annotation]
Copy link
Member

Choose a reason for hiding this comment

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

Even the bad py should have param type, right ? :)

return sum(numbers)


async def fetch_data(url): # [missing-return-type-annotation]
return await get(url)
13 changes: 13 additions & 0 deletions doc/data/messages/m/missing-return-type-annotation/details.rst
Original file line number Diff line number Diff line change
@@ -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:

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can also add function and methods starting with test_ for return-type ? I always found adding -> None in all tests to be rather pointless. But then mypy would disagree and we probably want to agree with mypy. (Btw just gave me an idea of a checker to check that function starting with test_ should not return anything).

- ``__init__`` methods (which implicitly return None)
- Abstract methods (``@abstractmethod``, ``@abstractproperty``)
- Properties and their setters/deleters
- Overload stub definitions (``@typing.overload``)
11 changes: 11 additions & 0 deletions doc/data/messages/m/missing-return-type-annotation/good.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions doc/whatsnew/fragments/3853.new_check
Original file line number Diff line number Diff line change
@@ -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
181 changes: 181 additions & 0 deletions pylint/checkers/type_annotations.py
Original file line number Diff line number Diff line change
@@ -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 = {
"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.",
),
"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.",
),
}

@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:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None:
visit_asyncfunctiondef = visit_functiondef

Sorry I can't select the whole function on mobile (or don't know how to)

"""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 delete methods (return value not meaningful)
if utils.decorated_with(
node, ["property", "*.setter", "*.deleter", "builtins.property"]
):
return

# Emit the message
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Skip abstract methods

Let's remove the other llm 'what should the llm write next' style comments too.

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))
Loading
Loading