Skip to content

Instantly share code, notes, and snippets.

@reillysiemens
Created December 19, 2018 05:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save reillysiemens/ef9c9f468aeea8d39ed8d87334872279 to your computer and use it in GitHub Desktop.
Save reillysiemens/ef9c9f468aeea8d39ed8d87334872279 to your computer and use it in GitHub Desktop.
Python Typing Protocol Default Class

This is perfectly fine Python. It outputs

Opening...
Opening...
1337
Closing...

when run, but Mypy complains...

example.py:29: error: Incompatible default for argument "cls" (default has type "Type[Concrete]", argument has type "SupportsOpenClose")
from typing import Any, List, Optional
from typing_extensions import Protocol
class SupportsOpenClose(Protocol):
def __call__(self) -> 'SupportsOpenClose':
...
def open(self, handles: List[str], *args: Any, **kwargs: Any) -> None:
...
def close(self) -> None:
...
class Concrete:
def __call__(self):
return self.__init__(self)
def open(self, handles: List[str], kw: Optional[int] = None) -> None:
print('Opening...')
if kw:
print(kw)
def close(self) -> None:
print('Closing...')
def fn(cls: SupportsOpenClose = Concrete):
opener_closer = cls()
opener_closer.open([])
opener_closer.open([], 1337)
opener_closer.close()
fn()
@reillysiemens
Copy link
Author

reillysiemens commented Dec 19, 2018

For posterity's sake the answer is that the structure in structural subtyping is important. The initial example doesn't work because the signature of open is indeed different from the protocol's definition. The error message from Mypy could be a little less confusing, but it's not wrong. 😅

So, in the end, we can write even simpler code and something like

from typing import Any, Callable, List
from typing_extensions import Protocol


class SupportsOpenClose(Protocol):
    def open(self, handles: List[str], *args: Any, **kwargs: Any) -> None:
        ...

    def close(self) -> None:
        ...


class Concrete:
    kw = 42

    def open(self, handles: List[str], *args: Any, **kwargs: Any) -> None:
        print('Opening...')
        kw = kwargs['kw'] if 'kw' in kwargs else self.kw
        print(f"kw is {kw}")

    def close(self) -> None:
        print('Closing...')


def fn(cls: Callable[[], SupportsOpenClose] = Concrete):
    opener_closer = cls()
    opener_closer.open([])
    opener_closer.open([], kw=1337)
    opener_closer.close()


fn()

works just fine. The key (no pun intended) is to force the Concrete.open signature to match the protocol signature exactly and use kwargs as a dictionary for communicating keyword arguments. There's an added benefit here that cls: Callable[[], SupportsOpenClose] leaves the fn function even more open for extension. Any callable that resolves to a type that implements the SupportsOpenClose protocol will satisfy the signature, so it doesn't necessarily need to be a class.

Many thanks to @RadicalZephyr for helping me puzzle through this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment