Is there a way to enforce subclasses to implement all the desired methods?

from typing import Protocol


class ImplementALLThesePlease(Protocol):
    def do_this(self, a: int, b: str) -> None: ...
    def do_that(self, c: str, *, d: int = 1) -> str: ...


class SomeSubclass(ImplementALLThesePlease):
    def do_this(self, a: int, b: str) -> None:
        return None
    def do_that(self, c: str, d: int | None = None) -> str: # Wrong signature, no complaints
        return ""
    
b = SomeSubclass()  # no complaints here either

Is there a way to get vscode/mypy to complain when the exact method and signature are missing?

Personally in this case I’d use an abstract class instead of a protocol; have you tried that? https://docs.python.org/3/library/abc.html

Then mark the methods in the base class with @abc.abstractmethod.

The type checker should treat that similarly, and as a bonus (IMO) if you try to instantiate a subclass that doesn’t have all the abstract methods implemented, you get an error at runtime. :slightly_smiling_face:

from abc import ABC, abstractmethod


class ImplementALLThesePlease(ABC):
    @abstractmethod
    def do_this(self, a: int, b: str) -> None: ...
    @abstractmethod
    def do_that(self, c: str, *, d: int = 1) -> str: ...


class SomeSubclass(ImplementALLThesePlease):
    def do_this(self, a: int, b: str) -> None:
        return None

    def do_that(self, c: str, d: int | None = None) -> str:  # Wrong signature, no complaints
        return ""


b = SomeSubclass()  # no complaints here either

neither my IDE nor the typechecker complained about this either

So pyright does complain about it (both ABC and Protocol) but mypy doesn’t. Unfortunate, my project uses mypy and I would have loved to know if I’m accidentally not implementing a method correctly

I’m not much of a typing guy, but this sounds like a MyPy feature request to me.

like much (all?) of Python @abstractmethod is a run-time thing – but the point of MyPy is to capture as much static behavior as possible, this sure seems possible.

I’m actually surprised that Pyright did complain about it since the second do_that() method seems completely compatible with the first, even with d taking an extra type (None).

Turns out that method compatibility in Pyright only considers postition-only parameters as compatible but not keyword-only parameters. (See here.)

So according to Pyright’s rules, this is allowed:

class A:
    def f(self, d: int, /) -> None: ...
class B(A):
    def f(self, d: int) -> None: ...

But this is not allowed:

class A:
    def f(self, *, d: int) -> None: ...
class B(A):
    def f(self, d: int) -> None: ...