You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Protocols should not care if a type is a class or an instance #4536
As a developer of a protocol, I don't care whether that protocol is met by an instance or a class, as long as the necessary functions and variables are present and correct. Currently it appears that only instances can satisfy a protocol.
For example, the code below attempts to meet a protocol using an instance and a class. It executes in python fine, but mypy complains that Type[Jumper1] is not compatible with the Jumps protocol:
changed the title [-]Protocols should respect classmethods and class variables[/-][+]Protocols should not care if a type is a class or an instance[/+]on Feb 3, 2018
Zope Interface has this notion of "provides" as distinct from "implements" to draw a clean separation between the different ways a type can relate to an Interface; it seems like Protocol should behave in the same way. It has a directlyProvides function which can be used to annotate any object, including classes and modules, to indicate that it provides the interface.
I agree with the OP's initial example (and with #5018), but I think Glyph's example goes a bit too far -- it dynamically modifies an object. While in the toy example it is easy to see that nothing has a jump attribute, in realistic applications I doubt that in most cases mypy would be able to make the connection.
IIUC the provides/implements distinction is between a class whose instances can jump() vs. an instance that can jump()? (Though I'm not all that familiar with Zope Interface, and the two words don't have clearly distinct meanings to me -- I could easily have it backwards.)
IIUC the provides/implements distinction is between a class whose instances can jump() vs. an instance that can jump()?
Yes.
To restate:
An object x is said to "provide" the interface IX if a function that "takes an IX" can take x and behave correctly; i.e. it matches the specification presented by IX.
A type X is said to "implement" the interface IX if all its instances provide IX.
The distinction is important because if IX specifies methods a, b, and c, the type X might:
have all three of those attributes just because it implements the interface, but this of course doesn't imply that it provides said interface,
implement the interface using some runtime trickery and not itself have any of those attributes,
have all three of those attributes because it directly provides the interface; if it does so, depending on descriptor semantics, its instances may or may not provide the interface; i.e. it may provide but not implement the interface.
Regarding my setting-attributes-on-an-instance example, I agree that as stated it's perhaps pushing mypy a bit far. Practically I do not expect it to be able to do that automatically without help. What I want is maybe something like dynamic_cast in C++, for @runtimeProtocols only.
OK, so "provides" is a property of objects while "implements" is a property of types -- with the understanding that a class is both an object and a type. And the distinction is an important once especially when you're doing runtime checking and all you can check is the presence of specific attributes, rather than the full signatures of methods.
(I've got to admit that even after writing that down the choice of terms still feels a bit arbitrary. Especially "implements" could go either way in my intuition.)
In general the checks that mypy performs are about "provides" -- e.g. we have a function foo:
classP(Protocol): ...
deffoo(arg: P):
...
and we're calling it with an expression:
foo(expr)
then mypy checks whether expr provides P. In the usual case, this boils down to checking whether the type of exprimplementsP.
But when expr is a class object, checking whether that class provides P roughly requires looking for matches of P's methods among that class's class methods and static methods. (We should first check whether its metaclass implements P, given that classes are objects.)
Finally when expr is a module object we should again check whether the module provides P by matching the functions it defines against the methods of P (again after checking whether ModuleType might implement P).
I suppose in the wonderful world of Zope Interfaces there are other cases to consider, since providing an interface could be done dynamically. But mypy is no help here, and I think the examples of dynamically providing an interface that I've seen all deserve the term "hack". :-)
A small note about this, mypy should accept not only Type[X] as an implementation, but also a Callable, if the fallback is appropriate. The latter actually currently works but only partially, in particular __call__ is lost when switching to the fallback instance.
Also I raise priority to high, since this is a required part of PEP 544 in its accepted version.
Seeing how the issue is still open, and how the described behaviour is still observed, I have to ask: are there any known workarounds for this outside of completely disabling typechecks for the relevant code parts?
Total redesign involving getting rid of the classmethods is not always an option.
@matejcik (or anyone else on this issue) apologies for the somewhat random ping, but I'd be interested in knowing if you have any open source code that makes use of this feature
@hauntsaninja sure, two off the top of my head
one: the PathSchemaType is a very simple protocol built off the PathSchema class, but also implemented by "static" versions AlwaysMatchingSchema and NeverMatchingSchema could be singletons, but there's also zero reason to do it that way. (esp. given that this is in a constrained environment, I don't want to have to have useless instances around)
also in this case I could technically replace the protocol with a Callable[[arg], bool], but then I'd have to pre-filter my PathSchema objects when passing them around, or implement __call__ or do something similarly distasteful in the name of satisfying the typechecker.
two: this protocol is semantically a protocol in that it is just a namespace providing some functions. One implementation is a class whose instances have some state. The other is basically just a namespace; it would be perfectly good as a module except that we wanted to have it in the same file.
This is causing a new issue with dataclass protocols. In my case, I need to verify that a ConfigurationClass is a dataclass, or has a dataclass_fields attribute. This changes gets us a lot closer, but It looks like there is a problem when both the Class and Instances provide the same interface
error: Argument 2 to "Configuration" has incompatible type "Type[GlobalConfig]"; expected "ConfigProtocol"
note: Only class variables allowed for class object access on protocols, __dataclass_fields__ is an instance variable of "GlobalConfig"
note: Only class variables allowed for class object access on protocols, __module__ is an instance variable of "GlobalConfig"
In this particular case, the values are identical for both the Class and Instance objects:
In []: GlobalConfig.__dataclass_fields__==gc.__dataclass_fields__Out[]: TrueIn []: GlobalConfig.__module__==gc.__module__Out[]: True
@lundybernard thanks for testing! I think we just need to mark __dataclass_fields__ as a ClassVar, so #13721 should fix.
We'd need a similar change in typeshed for __module__python/typeshed#8787, although you should probably just remove it from the Protocol because it's not going to do anything (since object is typed as having __module__).
@lundybernard thanks for testing! I think we just need to mark __dataclass_fields__ as a ClassVar, so #13721 should fix.
We'd need a similar change in typeshed for __module__python/typeshed#8787, although you should probably just remove it from the Protocol because it's not going to do anything (since object is typed as having __module__).
Thanks! I'll test out #13721 when it gets merged, or I have some extra time.
module is technically a requirement, as our code needs to read it, but I suppose its safe to assume it will always exist on any object capable of satisfying the Protocol.
Activity
[-]Protocols should respect classmethods and class variables[/-][+]Protocols should not care if a type is a class or an instance[/+]glyph commentedon Jun 7, 2018
I just hit this as well. It looks like this and #5018 are the same issue, but about modules rather than classes.
I think the same thing probably should be true of instances as well. If I do, for example:
I feel like that ought to work.
Zope Interface has this notion of "provides" as distinct from "implements" to draw a clean separation between the different ways a type can relate to an Interface; it seems like Protocol should behave in the same way. It has a
directlyProvidesfunction which can be used to annotate any object, including classes and modules, to indicate that it provides the interface.gvanrossum commentedon Jun 8, 2018
I agree with the OP's initial example (and with #5018), but I think Glyph's example goes a bit too far -- it dynamically modifies an object. While in the toy example it is easy to see that nothing has a jump attribute, in realistic applications I doubt that in most cases mypy would be able to make the connection.
IIUC the provides/implements distinction is between a class whose instances can jump() vs. an instance that can jump()? (Though I'm not all that familiar with Zope Interface, and the two words don't have clearly distinct meanings to me -- I could easily have it backwards.)
ilevkivskyi commentedon Jun 8, 2018
The same for me.
ilevkivskyi commentedon Jun 8, 2018
Note to self: we need to add an OP example (simplified) to the PEP 544 (there is already a PR for modules).
glyph commentedon Jun 14, 2018
Yes.
To restate:
An object
xis said to "provide" the interfaceIXif a function that "takes anIX" can takexand behave correctly; i.e. it matches the specification presented byIX.A type
Xis said to "implement" the interfaceIXif all its instances provideIX.The distinction is important because if
IXspecifies methodsa,b, andc, the typeXmight:glyph commentedon Jun 14, 2018
Regarding my setting-attributes-on-an-instance example, I agree that as stated it's perhaps pushing mypy a bit far. Practically I do not expect it to be able to do that automatically without help. What I want is maybe something like
dynamic_castin C++, for@runtimeProtocols only.gvanrossum commentedon Jun 19, 2018
OK, so "provides" is a property of objects while "implements" is a property of types -- with the understanding that a class is both an object and a type. And the distinction is an important once especially when you're doing runtime checking and all you can check is the presence of specific attributes, rather than the full signatures of methods.
(I've got to admit that even after writing that down the choice of terms still feels a bit arbitrary. Especially "implements" could go either way in my intuition.)
In general the checks that mypy performs are about "provides" -- e.g. we have a function foo:
and we're calling it with an expression:
then mypy checks whether
exprprovidesP. In the usual case, this boils down to checking whether the type ofexprimplementsP.But when
expris a class object, checking whether that class providesProughly requires looking for matches ofP's methods among that class's class methods and static methods. (We should first check whether its metaclass implementsP, given that classes are objects.)Finally when
expris a module object we should again check whether the module providesPby matching the functions it defines against the methods ofP(again after checking whetherModuleTypemight implementP).I suppose in the wonderful world of Zope Interfaces there are other cases to consider, since providing an interface could be done dynamically. But mypy is no help here, and I think the examples of dynamically providing an interface that I've seen all deserve the term "hack". :-)
Minor tweaks to PEP 544 (#1046)
ilevkivskyi commentedon Nov 2, 2019
A small note about this, mypy should accept not only
Type[X]as an implementation, but also aCallable, if the fallback is appropriate. The latter actually currently works but only partially, in particular__call__is lost when switching to the fallback instance.Also I raise priority to high, since this is a required part of PEP 544 in its accepted version.
1 remaining item
the-vindicar commentedon Jun 11, 2021
Seeing how the issue is still open, and how the described behaviour is still observed, I have to ask: are there any known workarounds for this outside of completely disabling typechecks for the relevant code parts?
Total redesign involving getting rid of the classmethods is not always an option.
dam5h commentedon Sep 15, 2021
Bumping this as I just ran into it as well, any workarounds?
Allow classes as protocol implementations (#13501)
gvanrossum commentedon Aug 25, 2022
Cool! What about modules? Can they implement protocols?
ilevkivskyi commentedon Aug 25, 2022
Sure, tomorrow.
hauntsaninja commentedon Aug 25, 2022
#5018 :-)
hauntsaninja commentedon Aug 25, 2022
@matejcik (or anyone else on this issue) apologies for the somewhat random ping, but I'd be interested in knowing if you have any open source code that makes use of this feature
matejcik commentedon Aug 26, 2022
@hauntsaninja sure, two off the top of my head
one: the
PathSchemaTypeis a very simple protocol built off thePathSchemaclass, but also implemented by "static" versionsAlwaysMatchingSchemaandNeverMatchingSchemacould be singletons, but there's also zero reason to do it that way. (esp. given that this is in a constrained environment, I don't want to have to have useless instances around)also in this case I could technically replace the protocol with a
Callable[[arg], bool], but then I'd have to pre-filter myPathSchemaobjects when passing them around, or implement__call__or do something similarly distasteful in the name of satisfying the typechecker.two: this protocol is semantically a protocol in that it is just a namespace providing some functions.
One implementation is a class whose instances have some state.
The other is basically just a namespace; it would be perfectly good as a module except that we wanted to have it in the same file.
hauntsaninja commentedon Aug 26, 2022
Awesome, thank you! I'll add https://github.com/trezor/trezor-firmware to https://github.com/hauntsaninja/mypy_primer once the next release is out
lundybernard commentedon Sep 23, 2022
This is causing a new issue with dataclass protocols. In my case, I need to verify that a ConfigurationClass is a dataclass, or has a dataclass_fields attribute. This changes gets us a lot closer, but It looks like there is a problem when both the Class and Instances provide the same interface
Results in a new mypy error:
In this particular case, the values are identical for both the Class and Instance objects:
hauntsaninja commentedon Sep 23, 2022
@lundybernard thanks for testing! I think we just need to mark
__dataclass_fields__as a ClassVar, so #13721 should fix.We'd need a similar change in typeshed for
__module__python/typeshed#8787, although you should probably just remove it from the Protocol because it's not going to do anything (since object is typed as having__module__).lundybernard commentedon Sep 24, 2022
Thanks! I'll test out #13721 when it gets merged, or I have some extra time.
module is technically a requirement, as our code needs to read it, but I suppose its safe to assume it will always exist on any object capable of satisfying the Protocol.