How to only type the first positional parameter of a Protocol method and let the others be untyped?
Example, having a protocol named MyProtocol
that has a method named my_method
that requires only the first positional parameter to be an int, while letting the rest be untyped.
the following class would implement it correctly without error:
class Imp1(MyProtocol):
def my_method(self, first_param: int, x: float, y: float) -> int:
return int(first_param - x + y)
However the following implementation wouldn't implement it correctly, since the first parameter is a float:
class Imp2(MyProtocol):
def my_method(self, x: float, y: float) -> int: # Error, method must have a int parameter as a first argument after self
return int(x+y)
I thought I would be able to do that with *args
, and **kwargs
combined with Protocol
like so:
from typing import Protocol, Any
class MyProtocol(Protocol):
def my_method(self, first_param: int, /, *args: Any, **kwargs: Any) -> int:
...
But (in mypy) this makes both Imp1 and Imp2 fail, because it forces the method contract to really have a *args
, **kwargs
like so:
class Imp3(MyProtocol):
def my_method(self, first_param: int, /, *args: Any, **kwargs: Any) -> int:
return first_param
But this does not solves what I am trying to achieve, that is make the implementation class have any typed/untyped parameters except for the first parameter.
I manged to circumvent the issue by using an abstract class with a setter set_first_param
, like so:
from abc import ABC, abstractmethod
from typing import Any
class MyAbstractClass(ABC):
_first_param: int
def set_first_param(self, first_param: int):
self._first_param = first_param
@abstractmethod
def my_method(self, *args: Any, **kwargs: Any) -> int:
...
class AbcImp1(MyAbstractClass):
def my_method(self, x: float, y: float) -> int:
return int(self._first_param + x - y) # now i can access the first_parameter with self._first_param
But this totally changes the initial API that I am trying to achieve, and in my opinion makes less clear to the implementation method that this parameter will be set before calling my_method
.
This example was tested using python version 3.9.13
and mypy version 0.991
.
If your MyProtocol
can accept any number of arguments, you cannot have a subtype (or implementation) which accepts a set number, this breaks the Liskov substitution principle as the subtype only accepts a limited set of cases accepted by the supertype.
[original paragraph]
Then, if you keep on inheriting from Protocol
, you keep on making protocols, protocols are different from ABC
s, they use structural subtyping (not nominal subtyping), meaning that as long as an object implements all the methods/properties of a protocol it is an instance of that protocol (see PEP 544 for more details).
[end original paragraph]
[edit upon further reading]
In my opinion, protocols should only be inherited by other protocols which will be used with structural subtyping. For nominal subtyping (which for instance allows default implementation) I would use ABCs.
[edit upon further reading]
Without more detail on the implementations you'd want to use, @blhsing's solution is probably the most open because it does not type the Callable's call signature.
Here is a set of implementations around a generic protocol with contravariant types (bound to float as it is the top of the numeric tower), which would allow any numeric type for the two x
and y
arguments.
from typing import Any, Generic, Protocol, TypeVar
T = TypeVar("T", contravariant=True, bound=float)
U = TypeVar("U", contravariant=True, bound=float)
class MyProtocol(Protocol[T, U]):
def my_method(self, first_param: int, x: T, y: U) -> int:
...
class ImplementMyProtocol1(Generic[T, U]):
"""Generic implementation, needs typing"""
def my_method(self, first_param: int, x: T, y: U) -> int:
return int(first_param - x + y)
class ImplementMyProtocol2:
"""Float implementation, and ignores first argument"""
def my_method(self, _: int, x: float, y: float) -> int:
return int(x + y)
class ImplementMyProtocol3:
"""Another float implementation, with and extension"""
def my_method(self, first_param: int, x: float, y: float, *args: float) -> int:
return int(first_param - x + y + sum(args))
def use_MyProtocol(inst: MyProtocol[T, U], n: int, x: T, y: U) -> int:
return inst.my_method(n, x, y)
use_MyProtocol(ImplementMyProtocol1[float, float](), 1, 2.0, 3.0) # OK MyProtocol[float, float]
use_MyProtocol(ImplementMyProtocol1[int, int](), 1, 2, 3) # OK MyProtocol[int, int]
use_MyProtocol(ImplementMyProtocol2(), 1, 2.0, 3.0) # OK MyProtocol[float, float]
use_MyProtocol(ImplementMyProtocol3(), 1, 2.0, 3.0) # OK MyProtocol[float, float]