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

2

12

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?
Wainscot answered 7/7, 2020 at 4:49 Comment(2)
This issue in the Python bug tracker looks related: bugs.python.org/issue44807 ... So we may see some updates to Protocol/__init__ interaction in Python version 3.11.Wainscot
This may also be related: github.com/python/typing/issues/644Wainscot
B
16

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.

Barmecide answered 7/7, 2020 at 4:57 Comment(0)
W
0

Summary

This has been fixed in later versions of Python (>=3.11) and in typing_extensions>=4.6.0.

Context

Per the comment

This issue in the Python bug tracker looks related: bugs.python.org/issue44807 ... So we may see some updates to Protocol/__init__ interaction in Python version 3.11.

Python 3.11 did address this problem

The __init__() method of Protocol subclasses is now preserved. (Contributed by Adrian Garcia Badarasco in gh-88970.)

For versions before 3.11, the typing_extensions>=4.6.0 package also has the fix.

Changed in version 4.6.0: Backported the ability to define __init__ methods on Protocol classes.

Example

Using import from typing_extensions==4.12.2

>>> from abc import ABC 
... from typing_extensions import Protocol  # DIFFERENT IMPORT
...  
... 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()  # now prints "ProtocolBase.__init__ called"          
AbstractBase.__init__ called
ProtocolBase.__init__ called
Weihs answered 29/7 at 21:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.