All code is available here: https://github.com/zhukovgreen/protocols-demo
-
-
Save zhukovgreen/8cedd8e6f0d4a776aad37aab59db426e to your computer and use it in GitHub Desktop.
Typing Callable is quite limited and truncated in a sense of type hinting support. Protocols can be used to annotate more complex types of callables.
from typing import Callable, Protocol
class ComplexCallableLike(Protocol):
def __call__(self, a: str, *, kwarg1: int) -> float: ...
def foo(self, a: str, *, kwarg1: int): ...
foo_like_callable: Callable[[str, int], float]
foo_like_protocol: ComplexCallableLike
foo_like_callable()
foo_like_protocol()
Type-cheking:
error: Too few arguments [call-arg]
error: Missing positional argument "a" in call to "__call__" of "ComplexCallableLike" [call-arg]
error: Missing named argument "kwarg1" for "__call__" of "ComplexCallableLike" [call-arg]
Also the type hinting in the IDE:
- Callable:
- Protocol
When protocol is implemented by inheriting the class from it, this called explicit implementation of protocols. See the following snippet:
from typing import Protocol
import attrs
dataclass = attrs.define(auto_attribs=True, frozen=True)
@dataclass
class ExplicitBaseA(Protocol):
attr1: str
attr2: str
@dataclass
class ImplicitBaseA(Protocol):
attr3: str
attr4: str
@dataclass
class ImplementsImplicitBaseA: ...
ImplementsImplicitBaseA()
@dataclass
class ImplementsExplicitBaseA(ExplicitBaseA): ...
ImplementsExplicitBaseA()
When type-checking with mypy:
$ mypy ./implicit_vs_explicit.py
src/protocols_demo/implicit_vs_explicit.py:32: error:
Cannot instantiate abstract class "ImplementsExplicitBaseA"
with abstract attributes "attr1" and "attr2" [abstract]
Notice, that ImplementsImplicitBaseA is type-checked well, since mypy do not know yet, that this is suppose to be ImplicitBaseA.
As well as IDE - it doesn't know this fact and do not play its role (speeding you up) well compared to ImplementsExplicitBaseA case.
Other advantage of explicit protocols is that you can bind a default implementations to it.
from typing import Protocol
import attrs
dataclass = attrs.define(auto_attribs=True, frozen=True)
@dataclass
class ExplicitBaseA(Protocol):
attr1: str
attr2: str
attr_with_default: int = 15
def method(self) -> str: ...
def method_with_default(self) -> str:
return "default"
@dataclass
class ImplementsExplicitBaseA(ExplicitBaseA): ...
ImplementsExplicitBaseA()
When type checking:
error: Cannot instantiate abstract class "ImplementsExplicitBaseA" with abstract attributes "attr1", "attr2" and "method" [abstract]
Notice that attr_with_default and method_with_default is not specified in the error.
Nested protocols allows extending protocols by other protocols, including basic inheritance or mixin approaches. It is just important to add Protocol at the end when inheriting. For example:
from typing import Protocol
import attrs
dataclass = attrs.define(auto_attribs=True, frozen=True)
@dataclass
class Human(Protocol):
attr1: str
attr2: str
@dataclass
class GoodHuman(Human, Protocol):
attr3: str
@dataclass
class BadHuman(Human, Protocol):
attr4: str
class ImplementGoodHuman(GoodHuman): ...
class ImplementBadHuman(BadHuman): ...
ImplementGoodHuman()
ImplementBadHuman()
class CanJump(Protocol):
def jump(self) -> None: ...
class CanSwim(Protocol):
def smim(self) -> None: ...
class JumpoSwim(CanJump, CanSwim, Protocol): ...
class RealJumpoSmim(JumpoSwim): ...
RealJumpoSmim()
When type-checking:
error: Cannot instantiate abstract class "ImplementGoodHuman" with abstract attributes "attr1", "attr2" and "attr3" [abstract]
error: Cannot instantiate abstract class "ImplementBadHuman" with abstract attributes "attr1", "attr2" and "attr4" [abstract]
error: Cannot instantiate abstract class "RealJumpoSmim" with abstract attributes "jump" and "smim" [abstract]