If a function returns a subclass of a Protocol, what is the recommended type-hint for the return type of that function?
The following is a simplified piece of code for representation
from typing import Protocol, Type
from abc import abstractmethod
class Template(Protocol):
@abstractmethod
def display(self) -> None:
...
class Concrete1(Template):
def __init__(self, grade: int) -> None:
self._grade = grade
def display(self) -> None:
print(f"Displaying {self._grade}")
class Concrete2(Template):
def __init__(self, name: str) -> None:
self._name = name
def display(self) -> None:
print(f"Printing {self._name}")
def give_class(type: int) -> Type[Template]:
if type == 1:
return Concrete1
else:
return Concrete2
concrete_class = give_class(1)
concrete_class(5)
In the line concrete_class(5), Pylance informs Expected no arguments to "Template" constructor.
Protocols are not ABCs
Let me start of by emphasizing that protocols were introduced specifically so you do not have to define a nominal subclass to create a subtype relation. That is why it is called structural subtyping. To quote PEP 544, the goal was
While you can subclass a protocol explicitly when defining a concrete class, that is not what they were designed for.
Protocols are not abstract base classes. By using your protocol like an ABC, you are basically discarding everything that makes a protocol useful in the first place.
Your error and possible solutions
As to why you are getting that error, that is easily explained. Your
Templateprotocol does not define its own__init__method. When a variable is declared to be of typetype[Template](i.e. a class implementing theTemplateprotocol) and you want to instantiate it, the type checker will see thatTemplatedoes not define an__init__and fall back to theobject.__init__, which takes no arguments. Thus, providing an argument to the constructor is correctly marked as an error.Since you want to use your protocol not only to annotate pure instances that follow it, but also classes that you want to instantiate (i.e.
type[Template]), you need to think about the__init__method. If you want to express that for a class to implement yourTemplateprotocol it can have any constructor whatsoever, you should include such a permissive__init__signature in the protocol, for example:If you want to be more specific/restrictive, that is possible of course. You could for example declare that
Template-compliant classes must take exactly one argument in their__init__, but it can be of any type:Both of these solutions would work in your example. The latter however would restrict you from passing a keyword-argument to the constructor with any name other than
_argobviously.Proper structural subtyping
To conclude, I would suggest you actually utilize the power of protocols properly to allow for structural subtyping and get rid of the explicit subclassing and
abstractmethoddecorators. If all you care about is a fairly general constructor and yourdisplaymethod, you can achieve that like this:This passes
mypy --strictwithout errors (and should satisfy Pyright too). As you can see, bothConcrete1andConcrete2are accepted as return values forgive_classbecause they both follow theTemplateprotocol.Proper use of ABCs
There are of course still valid applications for abstract base classes. For example, if you wanted to define an actual implementation of a method in your base class that itself calls an abstract method, subclassing that explicitly (nominal subtyping) can make perfect sense.
Example:
But that is a totally different use case. Here we have the benefit that
Concrete1andConcrete2are nominal subclasses ofTemplate, thus inheritcall_displayfrom it. Since they are nominal subclasses anyway, there is no need forTemplateto be a protocol.And all this is not to say that it is impossible to find applications, where it is useful for something to be both a protocol and an abstract base class. But such a use case should be properly justified and from the context of your question I really do not see any justification for it.