I'm starting to think that the "callback protocol" approach is not usable for this usecase, because an instance of a class with a __call__ method is not functionally equivalent to a function when assigned as a class attribute.

That's correct. When Python looks up a method from an instance, it calls __get__ to get a method object, which it then __call__()s. If there is no __get__, the object is called as is, without passing self.

Consider this, for example:

>> f = Foo() >>> f.method > >>> Foo.method >>> Foo.method.__get__(f) >">
>>> class Foo:
...     def method(self):
...             print("hi")
... 
>>> f = Foo()
>>> f.method
>
>>> Foo.method

>>> Foo.method.__get__(f)
>

For function objects, the __get__ method returns a method object, which ensures that self is passed: f.method, aka Foo.method.__get__(f), behaves a lot like functools.partial(Foo.method, f). But the return value of __get__ can be anything, and __get__() can do whatever it wants instead of even calling your method. This is how @property and @staticmethod work, for example.

To tell mypy that your object isn't just a callable, but a method, you need to explain to mypy that __get__ behaves just like shown above. You can do this by adding a __get__ method to your protocol. I believe it works, but I haven't tried it. Let me know if you have trouble getting this to work, and I'll try to write some example code :)

🌐
Real Python
realpython.com › python-protocol
Python Protocols: Leveraging Structural Subtyping – Real Python
July 25, 2024 - After decorating your Shape protocol ... you know how to use protocols in Python. Protocols let you define a type relationship between objects without the burden of inheritance....
🌐
Python
typing.python.org › en › latest › spec › protocol.html
Protocols — typing documentation
Type checkers should reject an isinstance() or issubclass() call, if there is an unsafe overlap between the type of the first argument and the protocol. Type checkers should be able to select a correct element from a union after a safe isinstance() or issubclass() call. For narrowing from non-union types, type checkers can use their best judgement (this is intentionally unspecified, since a precise specification would require intersection types). ... © Copyright 2021, The Python Typing Team.
Discussions

Is it possible to define a protocol for a method?
Mypy: ❌ Python: ✅ (since we only changed the annotated Protocol) ... test_dynamic_method_mypy.py:21: error: Missing positional argument "s" in call to "__call__" of "ConverterProtocol" Found 1 error in 1 file (checked 1 source file) I first thought this would be a bug in Mypy. And maybe it is? But I'm also thinking there is a nuanced difference between how a function ... More on github.com
🌐 github.com
2
2
January 19, 2022
How to provide type hint for a function that returns an Protocol subclass in Python? - Stack Overflow
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 im... More on stackoverflow.com
🌐 stackoverflow.com
python - How to combine a custom protocol with the Callable protocol? - Stack Overflow
I have a decorator that takes a function and returns the same function with some added attributes: import functools from typing import * def decorator(func: Callable) -> Callable: func.attr1... More on stackoverflow.com
🌐 stackoverflow.com
Functional Protocols - Ideas - Discussions on Python.org
Topic Introduction Has anyone ever considered adding protocols to Python that fit the more functional style? For example, we already have the built-in len which simply calls the __len__ magic method on implementers of the “Sized” protocol. Let’s say I want to create a new protocol, ... More on discuss.python.org
🌐 discuss.python.org
1
January 13, 2023
🌐
Mimo
mimo.org › glossary › python › protocol
Python Protocol: Syntax, Usage, and Examples
A Python protocol is a type hint that describes a shape of behavior instead of a specific class. It lets you say “anything with these methods and attributes is fine,” which fits perfectly with Python’s duck typing style. ... Become a Python developer. Master Python from basics to advanced ...
🌐
Python
typing.python.org › en › latest › reference › protocols.html
Protocols and structural subtyping — typing documentation
Predefined protocol reference lists all protocols defined in typing and the signatures of the corresponding methods you need to define to implement each protocol.
🌐
DEV Community
dev.to › fwojciec › protocols-and-composition-in-python-8mm
Protocols and Composition in Python - DEV Community
November 6, 2021 - Given the protocol, the concrete ... a + b · A protocol is Python's take on "structural subtyping" - it's a type that's effectively implemented by anything that matches the signature of the protocol's methods....
Top answer
1 of 1
4

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

allowing users to write [...] code without explicit base classes in the class definition.

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 Template protocol does not define its own __init__ method. When a variable is declared to be of type type[Template] (i.e. a class implementing the Template protocol) and you want to instantiate it, the type checker will see that Template does not define an __init__ and fall back to the object.__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 your Template protocol it can have any constructor whatsoever, you should include such a permissive __init__ signature in the protocol, for example:

class Template(Protocol):
    def __init__(self, *args: Any, **kwargs: Any) -> None: ...

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:

class Template(Protocol):
    def __init__(self, _arg: Any) -> None: ...

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 _arg obviously.


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 abstractmethod decorators. If all you care about is a fairly general constructor and your display method, you can achieve that like this:

from typing import Any, Protocol


class Template(Protocol):
    def __init__(self, _arg: Any) -> None: ...

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


class Concrete1:
    def __init__(self, grade: int) -> None:
        self._grade = grade

    def display(self) -> None:
        print(f"Displaying {self._grade}")


class Concrete2:
    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)

This passes mypy --strict without errors (and should satisfy Pyright too). As you can see, both Concrete1 and Concrete2 are accepted as return values for give_class because they both follow the Template protocol.


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:

from abc import ABC, abstractmethod
from typing import Any


class Template(ABC):
    @abstractmethod
    def __init__(self, _arg: Any) -> None: ...

    @abstractmethod
    def display(self) -> None: ...

    def call_display(self) -> None:
        self.display()


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)
obj = concrete_class(5)
obj.call_display()  # Displaying 5

But that is a totally different use case. Here we have the benefit that Concrete1 and Concrete2 are nominal subclasses of Template, thus inherit call_display from it. Since they are nominal subclasses anyway, there is no need for Template to 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.

Find elsewhere
🌐
Xebia
xebia.com › home › blog › protocols in python: why you need them
Protocols In Python: Why You Need Them | Xebia
July 25, 2022 - Static duck typing, enabled by protocols, allows Python to perform type checking based on the structure and methods of an object rather than its explicit class inheritance. How do protocols improve code flexibility? Protocols allow different classes to be compatible with functions or methods as long as they implement the required methods, promoting code reuse and flexibility.
🌐
OneUptime
oneuptime.com › home › blog › how to use protocol classes for type safety in python
How to Use Protocol Classes for Type Safety in Python
February 2, 2026 - Introduced in Python 3.8 through PEP 544, Protocol classes enable structural subtyping. Unlike traditional inheritance where you explicitly declare relationships, Protocols define interfaces implicitly - any class that implements the required methods is automatically compatible, no inheritance needed. Let's start with a simple example. Say you want to write a function ...
🌐
Medium
medium.com › @vaishnavisb809 › improving-python-code-a-simple-look-at-typing-protocols-and-mypy-37e7e840ccbc
Improving Python Code: A Simple Look at Typing Protocols and Mypy | by VAISHNAVI SB | Medium
October 17, 2024 - Modular Code: Using protocols simplifies your code by decoupling type checks from inheritance. Mypy is a powerful static type checker for Python that helps catch errors early by analyzing your code before it runs. It works alongside Python’s type hints (introduced in PEP 484) and ensures that your program follows the expected type rules at development time, not at runtime. Detecting Type Mismatches: By analyzing the function ...
🌐
Python.org
discuss.python.org › ideas
Functional Protocols - Ideas - Discussions on Python.org
January 13, 2023 - Topic Introduction Has anyone ever considered adding protocols to Python that fit the more functional style? For example, we already have the built-in len which simply calls the __len__ magic method on implementers of the “Sized” protocol. Let’s say I want to create a new protocol, ...
🌐
Medium
medium.com › @commbigo › python-typing-protocol-c2f6a60c0ac6
Python typing — Protocol
December 28, 2023 - Protocol can be applied to all members within the class, encompassing variables, functions, and abstract methods.
🌐
TestDriven.io
testdriven.io › tips › 9f452585-e673-4617-8f35-ac85ab413e14
Tips and Tricks - Interfaces in Python with Protocol Classes | TestDriven.io
This way your function/method defines what it needs. from typing import Protocol class ApplicationConfig: DEBUG = False SECRET_KEY = "secret-key" EMAIL_API_KEY = "api-key" # bad def send_email(config: ApplicationConfig): print(f"Send email using API key: {config.EMAIL_API_KEY}") # better class EmailConfig(Protocol): EMAIL_API_KEY: str def send_email_(config: EmailConfig): print(f"Send email using API key: {config.EMAIL_API_KEY}")
🌐
DEV Community
dev.to › rochacbruno › python-protocol-oriented-programming-1m0g
Python Protocol Oriented Programming - DEV Community
August 23, 2022 - def awesome_function(thing: CallableSummableSubscriptable): # accepts only objects that implements that ⬆️ protocol. # Protocols can be checked at runtime @runtime_checkable # Or checked using static analysers e.g: mypy · OOP Python is actually POP (Protocol Oriented Programming)
🌐
Python documentation
docs.python.org › 3 › library › typing.html
typing — Support for type hints
1 month ago - Callable cannot express complex signatures such as functions that take a variadic number of arguments, overloaded functions, or functions that have keyword-only parameters. However, these signatures can be expressed by defining a Protocol class with a __call__() method:
🌐
Medium
aignishant.medium.com › the-ultimate-guide-to-interfaces-protocols-and-abcs-in-python-8a991d7a4430
The Ultimate Guide to Interfaces, Protocols, and ABCs in Python | by Nishant Gupta | Medium
March 6, 2025 - Python’s flexible approach to object-oriented programming offers multiple mechanisms for creating abstract interfaces and enforcing consistent behavior across classes. Three key concepts — interfaces, protocols, and Abstract Base Classes (ABCs) — provide different ways to establish contracts and shared behaviors in your code.
🌐
Nickypy
nickypy.com › blog › python-protocols
Notes on Python Protocols
While I could go find all the possible ... when the argument implements bar(self) -> None. Protocols in Python (PEP 544) allow for statically checking whether an object implements a specific method (see interfaces in Go, or traits in Rust)....
🌐
YouTube
youtube.com › watch
Protocols in Python: Why You Need Them - presented by Rogier van der Geer - YouTube
EuroPython 2022 - Protocols in Python: Why You Need Them - presented by Rogier van der Geer[Liffey B on 2022-07-13]Protocols have been around since Python 3....
Published   October 19, 2022