Python with structural subtyping by Protocol

python

In Python that has no interface syntax, it can assure that the function can be called by checking the function existance with hasattr(). However, this method needs to insert asserting each time and the error cannot be known until runtime.

class ImplClass():
  def foo(self):
    print("ok")

class NoImplClass():
  pass

def call(d):
  assert hasattr(d, 'foo')
  d.foo()

if __name__ == "__main__":
  call(ImplClass())   # => ok
  call(NoImplClass()) # => AssertionError

If you describe Type Hints implemented in Python 3.5, mismatched inputs can be detected by static analysis by mypy before runtime, but implementating functions in a subclass cannot be enforced.

PythonのType Hintsとmypy - sambaiz-net

class BaseClass:
  def foo(self):
    print("please implement this")

class ImplClass(BaseClass):
  def foo(self):
    print("ok")

class NoImplClass(BaseClass):
  pass

def call(d: BaseClass):
  d.foo()

if __name__ == "__main__":
  call(BaseClass())   # => please implement this
  call(ImplClass())   # => ok
  call(NoImplClass()) # => please implement this

Abstract Base Classes of PEP3119 resolved these issues. If you define a function with @abstractmethod in a subclass of abc.ABC, mypy detects an error when creating an instance of a subclass that is not implemented.

from abc import ABC, abstractmethod

class AbstractClass(ABC):
  @abstractmethod
  def foo(self):
    pass

class ImplClass(AbstractClass):
  def foo(self):
    print("ok")

class NoImplClass(AbstractClass):
  pass

def call(d: AbstractClass):
  d.foo()

if __name__ == '__main__':
  # AbstractClass() # -> TypeError: Can't instantiate abstract class AbstractClass with abstract methods foo
  call(ImplClass()) # -> ok 
  # NoImplClass()   # -> TypeError: Can't instantiate abstract class ImplClass with abstract methods foo

However, it needs to be explicitly subclassed, so there is no flexibility like duck typing.

Protocol of PEP544 implemented in Python 3.8 enables structural subtyping (static duck typing) that can consider any class implemented specific variables and functions as a subtype without extending a common base class.

from typing import Protocol

class ProtocolClass(Protocol):
  def foo(self):
    pass

class ImplClass:
  def foo(self):
    print("ok")

class NoImplClass:
  pass

def call(d: ProtocolClass):
  d.foo()

if __name__ == '__main__':
  # ProtocolClass()     # -> Cannot instantiate protocol class "ProtocolClass"
  call(ImplClass())     # -> ok 
  # call(NoImplClass()) # -> Argument 1 to "call" has incompatible type "NoImplClass"; expected "ProtocolClass"