Functools.singledispatch should support PEP 544 protocols

Not sure if it’s an omission or intentional. An example from the stackoverflow question:

from typing import Protocol
from functools import singledispatch


class HasFoo(Protocol):
    foo: str


class FooBar:
    def __init__(self):
        self.foo = 'bar'


@singledispatch
def f(_) -> None:
    raise NotImplementedError


@f.register
def _(item: HasFoo) -> None:
    print(item.foo)


x = FooBar()
f(x)

Adding the runtime_checkable decorator to HasFoo does not change the behavior: NotImplementedError still gets raised.

2 Likes

This would be a big update to the singledispatch implementation. I’m not much of a user of singledispatch myself, so I couldn’t tell you if this is desirable, and I have no idea whether it is easy to add to the implementation. Maybe you could give it a go – if it turns out to be a small PR we might invoke the Zen of Python (“If the implementation is easy to explain, it may be a good idea”), but the StackOverflow question doesn’t look hugely popular, so I’m not sure a lot of people care.

I think this would probably significantly complicate the implementation, and possibly lead to surprising variations in the performance of singledispatch, depending on whether Protocols are being dispatched on or not. (But I’m open to having my mind changed if you can prove me wrong with a proof-of-concept PR! :slightly_smiling_face:)

Does it have to go under singledispatch?

To me singledispatch is picking the correct implementation based on the type of the argument. The idea with dispatching on a Protocol is to pick the correct implementation based on if the passed argument has the correct attributes and those attributes are of a specific type…at least per the description of OP in SO post. So runtime_checkable isn’t needed, and to my understanding doesn’t check for presence of attributes nor their types…well docs say runtime_checkable looks for presence of defined methods on protocol but not their signatures. Not sure if it checks for non method attributes.

This sort of decorator is very doable by itself I believe. I’d like to take a whack at this and share it.

1 Like

This is not easy.

I wrongly assumed that if you decorated HasFoo with runtime_checkable that issubclass(FooBar, HasFoo) would be True but it is False. isinstance(FooBar(), HasBar) is True under that circumstance but the semantics of it are bad. Take for example…isinstance(FooBar2(), HasFoo) is True

class FooBar2:
    def __init__(self):
        self.foo = 1

As for the issubclass bit, it was explained to me that due to foo being set on the instance but not present on the class that issubclass(FooBar, HasFoo) is False. That makes a lot more sense to me once explained.

Seems like a lot of weird edge cases to deal with and like it should almost be its own dispatching decorator

1 Like

In case it’s intentional behavior, does the current typing system allows to modify declaration of singledispatch so that mypy can warn about using Protocols?

Mypy already has to heavily special-case singledispatch in order to provide support for it. It could certainly add more special-casing to detect if a user is attempting to use singledispatch with a protocol. But that’s a mypy issue that should be discussed on the mypy issue tracker.

2 Likes