My question is simple. I have this protocol:
from typing import Protocol
class LauncherTemplateConfig(Protocol):
def launch_program_cmd(self, **kwargs) -> list[str]:
pass
And this implementation of the protocol, which I would expect mypy passes, but it does not:
from typing import Optional
from pathlib import Path
class MyLauncherTemplateConfig:
def launch_program_cmd(
self, some_arg: Optional[Path] = None, another_arg=1
) -> list[str]:
I would expect the parameters in MyLauncherTemplateConfig.launch_program_cmd
to be compatible with **kwargs
in the Protocol class.
Not sure if I am doing something wrong...
The general principle
If you want MyPy to accept that a certain class implements the interface defined in a Protocol
, a relevant method in the concrete implementation must be no less permissive in the arguments it will accept than the abstract version of that method as defined in the Protocol
. This is consistent with other principles of object-oriented programming such as the Liskov Substitution Principle.
The specific issue here
Your Protocol
defines an interface in which the launch_program_cmd
method can be called with any keyword-arguments, and not fail at runtime. Your concrete implementation does not satisfy this interface, as any keyword arguments other than some_arg
or another_arg
will cause the method to raise an error.
Possible solution
If you want MyPy to declare your class as a safe implementation of your Protocol
, you have two options. You can either adjust the signature of the method in the Protocol
to be more specific, or adjust the signature of the method in the concrete implementation to be more generic. In the case of the latter, you might do it like this:
from typing import Any, Protocol, Optional
from pathlib import Path
class LauncherTemplateConfig(Protocol):
def launch_program_cmd(self, **kwargs: Any) -> list[str]: ...
class MyLauncherTemplateConfig:
def launch_program_cmd(self, **kwargs: Any) -> list[str]:
some_arg: Optional[Path] = kwargs.get('some_arg')
another_arg: int = kwargs.get('another_arg', 1)
# and then the rest of your method
By using the dict.get
method, we can retain the default values in your implementation, but do it in a way that sticks to the generic signature of the method that was declared in the Protocol