18

While trying to update my code to be PEP-484 compliant (I'm using mypy 0.610) I've ran into the following report:

$ mypy mymodule --strict-optional --ignore-missing-imports --disallow-untyped-calls --python-version 3.6

myfile.py:154: error: Signature of "deliver" incompatible with supertype "MyClass"

MyClass:

from abc import abstractmethod

from typing import Any


class MyClass(object):

@abstractmethod
def deliver(self, *args: Any, **kwargs: Any) -> bool:
    raise NotImplementedError

myfile.py:

class MyImplementation(MyClass):

[...]

    def deliver(self, source_path: str,
                dest_branches: list,
                commit_msg: str = None,
                exclude_files: list = None) -> bool:

        [...]

        return True

I'm definitely doing something wrong here, but I can't quite understand what :)

Any pointers would be much appreciated.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Pawel
  • 343
  • 1
  • 2
  • 7

3 Answers3

21
@abstractmethod
def deliver(self, *args: Any, **kwargs: Any) -> bool:
    raise NotImplementedError

This declaration doesn't mean subclasses can give deliver any signature they want. Subclass deliver methods must be ready to accept any arguments the superclass deliver method would accept, so your subclass deliver has to be ready to accept arbitrary positional or keyword arguments:

# omitting annotations
def deliver(self, *args, **kwargs):
    ...

Your subclass's deliver does not have that signature.


If all subclasses are supposed to have the same deliver signature you've written for MyImplementation, then you should give MyClass.deliver the same signature too. If your subclasses are going to have different deliver signatures, maybe this method shouldn't really be in the superclass, or maybe you need to rethink your class hierarchy, or give them the same signature.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 1
    Thanks for your reply!, really appreciate it :). This makes total sense. I think my error was to think that `*args, **kwargs` would give me total freedom to implement any signature I want for that method on child objects. Then, my follow up question would be if it's a bad practice to follow the same signature as the superclass and then comment (annotate maybe?) the expected params for that implementation. – Pawel Jun 24 '18 at 15:10
  • @Pawel: I would definitely consider that bad practice. – user2357112 Jun 24 '18 at 15:12
  • Yeah, again, it makes sense. And I don't want to work out of the boundaries of PEP-484, but at the same time I think (for my scenario, at least) each subclass should implement the `deliver` method the way they want. I guess some rethinking needs to be put in place :) – Pawel Jun 24 '18 at 15:21
  • As far as I can see, this signature would accept any arguments that the superclass method accepts: `def deliver(self, source_path: str, *args: Any, **kwargs: Any) -> bool:`. mypy still complains that he signature is incompatible. – Seppo Enarvi Apr 01 '22 at 09:32
  • 1
    @SeppoEnarvi: The superclass signature accepts any combination of arguments whatsoever. Your signature requires a `source_path` argument. – user2357112 Apr 01 '22 at 10:36
  • 1
    @Pawel: Did you get around to knowing how to rewrite this type of superclass and subclass? In a way that can show the relationship of the superclass and subclass & subclass method `deliver` could have a different signature. – NpnSaddy Dec 30 '22 at 07:19
-2

You can solve the question by using Callable[..., Any] and type: ignore such like bellow.

from typing import Callable

class MyClass(object):
    deliver: Callable[..., bool]
    @abstractmethod
    def deliver(self, *args, **kwargs): # type: ignore
        raise NotImplementedError
tasuren
  • 181
  • 1
  • 7
-3

Maybe you should work it around this way:

  1. Define abstract method without arguments:

    class MyClass:
        @abstractmethod
        def deliver(self) -> bool:
            raise NotImplementedError
    
  2. In implementations get all your data from self:

    class MyImplementation(MyClass):
        def __init__(
                self,
                source_path: str,
                dest_branches: list,
                commit_msg: str = None,
                exclude_files: list = None
        ) -> None:
            super().__init__()
            self.source_path = source_path
            self.dest_branches = dest_branches
            self.commit_msg = commit_msg
            self.exclude_files = exclude_files
    
        def deliver(self) -> bool:
            # some logic
            if self.source_path and self.commit_msg:
                return True
            return False
    

This way you will have completely compatible method declarations and still can implement methods as you want.

Andrey Semakin
  • 2,032
  • 1
  • 23
  • 50