pylint icon indicating copy to clipboard operation
pylint copied to clipboard

False positive: overriding a generic `ParamSpec` method triggers `arguments-differ` (W0221)

Open ruro opened this issue 1 year ago • 0 comments

Bug description

Consider the following pattern

example1.py
# pylint: disable=missing-docstring
class Base:
    @classmethod
    def apply(cls, *args, **kwargs):
        return cls.forward("ctx", *args, **kwargs)

    @staticmethod
    def forward(ctx, *args, **kwargs):
        raise NotImplementedError()


class Derived(Base):
    @staticmethod
    def forward(ctx, x, y):
        del x, y
        return ctx


Derived.apply(1, 2)

running pylint over this produces the following warnings:

************* Module example
example.py:14:4: W0221: Number of parameters was 3 in 'Base.forward' and is now 3 in overriding 'Derived.forward' method (arguments-differ)
example.py:14:4: W0221: Variadics removed in overriding 'Derived.forward' method (arguments-differ)

Now, this is somewhat understandable since the signature of Derived.forward is narrower than Base.forward, but conceptually, that is exactly how this pattern is supposed to be used (forward is supposed to only be called by apply and apply is supposed to directly forward its arguments).

I attempted to add some typing annotations using ParamSpec to make this relation more explicit:

example2.py
# pylint: disable=missing-docstring
from typing import ParamSpec, Generic, Any


P = ParamSpec("P")


class Base(Generic[P]):
    @classmethod
    def apply(cls, *args: P.args, **kwargs: P.kwargs) -> Any:
        return cls.forward("ctx", *args, **kwargs)

    @staticmethod
    def forward(ctx: str, *args: P.args, **kwargs: P.kwargs) -> Any:
        raise NotImplementedError()


class Derived(Base[int, int]):
    @staticmethod
    def forward(ctx: str, x: int, y: int) -> Any:
        del x, y
        return ctx


Derived.apply(1, 2)

But this still causes the same warnings.

By the way, mypy --strict accepts the version annotated with ParamSpec as well as this:

example3.py
# pylint: disable=missing-docstring
from typing import Any


class Base:
    @classmethod
    def apply(cls, *args: Any, **kwargs: Any) -> Any:
        return cls.forward("ctx", *args, **kwargs)

    @staticmethod
    def forward(ctx: str, *args: Any, **kwargs: Any) -> Any:
        raise NotImplementedError()


class Derived(Base):
    @staticmethod
    def forward(ctx: str, x: int, y: int) -> Any:
        del x, y
        return ctx


Derived.apply(1, 2)

Also, see issue https://github.com/pytorch/pytorch/issues/119475 for a real world example of this pattern.

Pylint version

pylint 3.0.3
astroid 3.0.3
Python 3.11.7 (main, Dec  4 2023, 18:10:11) [GCC 13.2.0]

ruro avatar Feb 08 '24 19:02 ruro