pythonprotocolspython-typingabstract-base-class

typing.Protocol class `__init__` method not called during explicit subtype construction


Python's PEP 544 introduces typing.Protocol for structural subtyping, a.k.a. "static duck typing".

In this PEP's section on Merging and extending protocols, it is stated that

The general philosophy is that protocols are mostly like regular ABCs, but a static type checker will handle them specially.

Thus, one would expect to inherit from a subclass of typing.Protocol in much the same way that one expects to inherit from a subclasses of abc.ABC:

from abc import ABC
from typing import Protocol

class AbstractBase(ABC):
    def method(self):
        print("AbstractBase.method called")

class Concrete1(AbstractBase):
    ...

c1 = Concrete1()
c1.method()  # prints "AbstractBase.method called"

class ProtocolBase(Protocol):
    def method(self):
        print("ProtocolBase.method called")

class Concrete2(ProtocolBase):
    ...

c2 = Concrete2()
c2.method()  # prints "ProtocolBase.method called"

As expected, the concrete subclasses Concrete1 and Concrete2 inherit method from their respective superclasses. This behavior is documented in the Explicitly declaring implementation section of the PEP:

To explicitly declare that a certain class implements a given protocol, it can be used as a regular base class. In this case a class could use default implementations of protocol members.

...

Note that there is little difference between explicit and implicit subtypes, the main benefit of explicit subclassing is to get some protocol methods "for free".

However, when the protocol class implements the __init__ method, __init__ is not inherited by explicit subclasses of the protocol class. This is in contrast to subclasses of an ABC class, which do inherit the __init__ method:

from abc import ABC
from typing import Protocol

class AbstractBase(ABC):
    def __init__(self):
        print("AbstractBase.__init__ called")

class Concrete1(AbstractBase):
    ...

c1 = Concrete1()  # prints "AbstractBase.__init__ called"

class ProtocolBase(Protocol):
    def __init__(self):
        print("ProtocolBase.__init__ called")

class Concrete2(ProtocolBase):
    ...

c2 = Concrete2()  # NOTHING GETS PRINTED

We see that, Concrete1 inherits __init__ from AbstractBase, but Concrete2 does not inherit __init__ from ProtocolBase. This is in contrast to the previous example, where Concrete1 and Concrete2 both inherit method from their respective superclasses.

My questions are:

  1. What is the rationale behind not having __init__ inherited by explicit subtypes of a protocol class? Is there some type-theoretic reason for protocol classes not being able to supply an __init__ method "for free"?
  2. Is there any documentation concerning this discrepancy? Or is it a bug?

Solution

  • You can't instantiate a protocol class directly. This is currently implemented by replacing a protocol's __init__ with a method whose sole function is to enforce this restriction:

    def _no_init(self, *args, **kwargs):
        if type(self)._is_protocol:
            raise TypeError('Protocols cannot be instantiated')
    
    ...
    
    class Protocol(Generic, metaclass=_ProtocolMeta):
        ...
    
        def __init_subclass__(cls, *args, **kwargs):
            ...
            cls.__init__ = _no_init
    

    Your __init__ doesn't execute because it isn't there any more.

    This is pretty weird and messes with even more stuff than it looks like at first glance - for example, it interacts poorly with multiple inheritance, interrupting super().__init__ chains.