Package: typing-protocol-intersection
typing-protocol-intersectionA tiny Python 3 package that introduces Protocol intersections (forProtocols themselves
seePEP 544).
TheProtocolIntersectiontype tells mypy that an object implements multiple protocols.
It can be used either as a function parameter or as a return value.
A mypy plugin that ships with the package is required for this to work.
See theexamplessection below.Supported versionsThe plugin supports python 3.7 up to 3.12 and mypy >= 0.920 and <= 1.8.x.InstallationThetyping-protocol-intersectionpackage is pip-installable:pipinstalltyping-protocol-intersectionConfigurationAddtyping_protocol_intersection.mypy_plugintopluginsin mypy configuration:>catmypy.ini[mypy]plugins=typing_protocol_intersection.mypy_pluginExamplesSimple examplefromtypingimportProtocolfromtyping_protocol_intersectionimportProtocolIntersectionasHasclassX(Protocol):x:strclassY(Protocol):y:strdeffoo(xy:Has[X,Y])->None:# Note xy implements both X and Y, not just one of themprint(xy.x,xy.y)Complex example - valid programHere's a more complex example showing what you can write with the help of this mypy plugin:fromtypesimportSimpleNamespacefromtypingimportProtocol,Generic,TypeVar,Dictfromtyping_protocol_intersectionimportProtocolIntersectionasHasclassX(Protocol):x:strclassY(Protocol):y:strT=TypeVar("T")classBuilder(Generic[T]):def__init__(self)->None:super().__init__()self._d:Dict[str,str]={}defwith_x(self)->"Builder[Has[T, X]]":self._d["x"]="X"returnself# type: ignoredefwith_y(self)->"Builder[Has[T, Y]]":self._d["y"]="Y"returnself# type: ignoredefbuild(self)->T:returnSimpleNamespace(**self._d)# type: ignoreclassDesiredObject(X,Y,Protocol):passdefget_x_y_1(o:DesiredObject)->None:print(f"{o.x=};{o.y=}")defget_x_y_2(o:Has[X,Y])->None:print(f"{o.x=};{o.y=}")defmain()->None:valid_o=Builder().with_x().with_y().build()get_x_y_1(valid_o)get_x_y_2(valid_o)if__name__=="__main__":main()># with plugin>mypyexample.py
Success:noissuesfoundin1sourcefileComplex example - invalid programAnd here's how would the plugin help if you forgot to include one of the protocols while building an object:fromtypesimportSimpleNamespacefromtypingimportProtocol,Generic,TypeVar,Dictfromtyping_protocol_intersectionimportProtocolIntersectionasHasclassX(Protocol):x:strclassY(Protocol):y:strT=TypeVar("T")classBuilder(Generic[T]):def__init__(self)->None:super().__init__()self._d:Dict[str,str]={}defwith_x(self)->"Builder[Has[T, X]]":self._d["x"]="X"returnself# type: ignoredefwith_y(self)->"Builder[Has[T, Y]]":self._d["y"]="Y"returnself# type: ignoredefbuild(self)->T:returnSimpleNamespace(**self._d)# type: ignoreclassDesiredObject(X,Y,Protocol):passdefget_x_y_1(o:DesiredObject)->None:print(f"{o.x=};{o.y=}")defget_x_y_2(o:Has[X,Y])->None:print(f"{o.x=};{o.y=}")defmain()->None:valid_o=Builder().with_x().build()# <-- note no .with_y()get_x_y_1(valid_o)get_x_y_2(valid_o)if__name__=="__main__":main()># Note the real output would contain some invisible characters which were removed here.>mypyexample.py
example.py:40:15:error:Argument1to"get_x_y_1"hasincompatibletype"ProtocolIntersection[X]";expected"DesiredObject"[arg-type]example.py:40:15:note:"ProtocolIntersection"ismissingfollowing"DesiredObject"protocolmember:
example.py:40:15:note:y
example.py:41:15:error:Argument1to"get_x_y_2"hasincompatibletype"typing_protocol_intersection.types.ProtocolIntersection[X]";expected"typing_protocol_intersection.types.ProtocolIntersection[Y, X]"[arg-type]example.py:41:15:note:"ProtocolIntersection"ismissingfollowing"ProtocolIntersection"protocolmember:
example.py:41:15:note:y
Found2errorsin1file(checked1sourcefile)Recommended usageTheProtocolIntersectionclass name might seem a bit lengthy, but it's explicit, which is good.
For brevity and better readability, it's recommended to use an alias when importing, as seen in the examples above.fromtyping_protocol_intersectionimportProtocolIntersectionasHas