Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should we remove abstract-method? #10054

Open
mbyrnepr2 opened this issue Oct 29, 2024 · 7 comments
Open

Should we remove abstract-method? #10054

mbyrnepr2 opened this issue Oct 29, 2024 · 7 comments
Labels
Needs decision 🔒 Needs a decision before implemention or rejection Proposal 📨

Comments

@mbyrnepr2
Copy link
Member

Current problem

abstract-method documentation.

The documentation states that this message is emitted when an abstract method (i.e. raise NotImplementedError) is not overridden in concrete class.

A concrete class is one where all the abstract methods have been implemented.

The problem with abstract-method is that we don't know for sure if a class is intended to be a concrete class or not. The only way we would know for sure is if it does contain all of the implemented abstract methods - but by that point the check is not useful.

The following scenario (modified from one of the examples in the documentation) is valid Python but it would emit abstract-method:

class Pet:
    def make_sound(self):
        raise NotImplementedError


class Cat(
    Pet
):  #  W0223: Method 'make_sound' is abstract in class 'Pet' but is not overridden in child class 'Cat' (abstract-method)
    def jump_up(self):
        print("jump!")


class Tiger(Cat):
    """This is the concrete class"""
    def make_sound(self):
        print("rrr!")


Tiger().make_sound()
Tiger().jump_up()

The other example from the documentation emits abstract-method; however the base class does not inherit from abc.ABC, so this example emits a warning even though the class, which is not implementing the abstract method, instantiates without error:

import abc


class WildAnimal:
    @abc.abstractmethod
    def make_sound(self):
        pass


class Panther(WildAnimal):  # [abstract-method]
    pass


Panther().make_sound()

Desired solution

Deprecate and remove abstract-method.

Additional context

#9979
#7950
#3098

Related message https://pylint.readthedocs.io/en/latest/user_guide/messages/error/abstract-class-instantiated.html

@mbyrnepr2 mbyrnepr2 added Needs triage 📥 Just created, needs acknowledgment, triage, and proper labelling Proposal 📨 Needs decision 🔒 Needs a decision before implemention or rejection and removed Needs triage 📥 Just created, needs acknowledgment, triage, and proper labelling labels Oct 29, 2024
@clauspruefer
Copy link

Please check my comments in #5793 and #10192.

@mbyrnepr2
Copy link
Member Author

Thanks for the engagement.

As I understand your comments, the answer to the question - Should we remove abstract-method - would be no if Pylint were to define abstract methods strictly as those that are decorated by abc.abstractmethod.

What I would say is, my description on this issue probably wasn't clear enough.

The main takeaway of point 1. is that the Tiger class is the concrete one which implements the abstract method, and therefore, abstract-method should not be emitted on the Cat class (it shouldn't be emitted at all in this example).
The problem being that Pylint doesn't know enough here and I think the false-positive to value ratio doesn't feel justified; I noticed it in our primer results here.

Note the example doesn't use abc.abstractmethod but the same principle applies because the Pylint behaviour should be the same in either situation. However, point 2. in the description shows that if you do inherit from abc.ABC, then the message isn't emitted at all, which is a bug in itself, i.e:
This emits abstract-method:

from abc import abstractmethod


class Base:
    @abstractmethod
    def meow(self):
        print("MEOW")


class Cat(Base):
    def scratch(self):
        """Scratch my back"""

While this does not:

from abc import abstractmethod, ABC


class Base(ABC):
    @abstractmethod
    def meow(self):
        print("MEOW")


class Cat(Base):
    def scratch(self):
        """Scratch my back"""

Even if we fix the bug in point 2., I feel that abstract-method is too problematic because of it's scope to any subclass.
Also we do have abstract-class-instantiated which catches this issue at call-time.

@clauspruefer
Copy link

clauspruefer commented Jan 19, 2025

Even if we fix the bug in point 2., I feel that abstract-method is too problematic because of it's scope to any subclass. Also we do have abstract-class-instantiated which catches this issue at call-time.

That makes perfect sense and pylint more precise. But please also check my post in #5793, this could be relevant for abstract-class-instantiated (analyzing) too.

In my opinion a deriving abstract class definition (child class) must consist of the exact same definition (including parameter / count) as the base class definition. If not, then it can no longer call itself abstract.

For #5793 it would make sense to only check "arguments-differ" for abstract methods in child classes (or also remove).

Wrong:

import abc

class Base(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def _testmethod(self, arg1):
        pass

class Child1(Base):
    @abc.abstractmethod
    def _testmethod(self, arg1, arg2):
        print(arg1*arg2)

class Child2(Base):
    @abc.abstractmethod
    def _testmethod(self, arg1, arg2):
        print(arg1*arg2*2)

Correct (two real abstract definitions in the base must be overloaded in both child classes)

import abc

class Base(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def _testmethod(self, arg1):
        pass
    @abc.abstractmethod
    def _testmethod(self, arg1, arg2):
        pass

class Child1(Base):
    @abc.abstractmethod
    def _testmethod(self, arg1):
        print(arg1*1)
    def _testmethod(self, arg1, arg2):
        print(arg1*arg2)

class Child2(Base):
    @abc.abstractmethod
    def _testmethod(self, arg1):
        print(arg1*2)
    def _testmethod(self, arg1, arg2):
        print(arg1*arg2*2)

@mbyrnepr2
Copy link
Member Author

mbyrnepr2 commented Jan 19, 2025

Thanks for the clarification.
I think the goal of arguments-differ is to point out if the LSP is violated by having a different signature in the overriding method in the subclass.
So, I think that applies to method overriding in the subclass regardless if the overridden method is abstract or not.

I think there is an issue with your example, because this isn't overloading. In other words, _testmethod(self, arg1) in Child1 and Child2 are replaced by the subsequent method with the same name - _testmethod(self, arg1, arg2).
Calling Child2(). _testmethod("hey") gives:
TypeError: Child2._testmethod() missing 1 required positional argument: 'arg2'

@clauspruefer
Copy link

Thanks for the clarification. I think the goal of arguments-differ is to point out if the LSP is violated by having a different signature in the overriding method in the subclass. So, I think that applies to method overriding in the subclass regardless if the overridden method is abstract or not.

I think there is an issue with your example, because this isn't overloading. In other words, _testmethod(self, arg1) in Child1 and Child2 are replaced by the subsequent method with the same name - _testmethod(self, arg1, arg2). Calling Child2(). _testmethod("hey") gives: TypeError: Child2._testmethod() missing 1 required positional argument: 'arg2'

Understand. My assumption of Python class method overloading in combination with positional parameters was wrong, which Pylint corrected now 👍.

@clauspruefer
Copy link

But returning to the current problem.

Consider: An abstract class method is only abstract if it is decorated with the abc.ABCMeta @abc.abstractmethoddecorator.
It is impossible to differentiate otherwise.

@mbyrnepr2
Copy link
Member Author

But returning to the current problem.

Consider: An abstract class method is only abstract if it is decorated with the abc.ABCMeta @abc.abstractmethoddecorator.
It is impossible to differentiate otherwise.

I guess we should keep discussion about that on the #10192 issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs decision 🔒 Needs a decision before implemention or rejection Proposal 📨
Projects
None yet
Development

No branches or pull requests

2 participants