Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type checking python function signatures of Protocol subclass with mypy

Tags:

python

types

mypy

Is there a way to safely type-check a python class which subclasses a protocol?

If I define a protocol with a certain method function signature, then implicit subclasses must define a method with a compatible signature:

# protocols.py

from abc import abstractmethod
from dataclasses import dataclass
from typing import Protocol

class SupportsPublish(Protocol):
    @abstractmethod
    def publish(self, topic: str):
        ...

def publish(m: SupportsPublish):
    m.publish("topic")

@dataclass
class Publishable:
    foo: str = "bar"

    def publish(self):
        print(self)

publish(Publishable())

# ✗ mypy protocols.py       
# protocols.py:24: error: Argument 1 to "publish" has incompatible type "Publishable"; expected "SupportsPublish"  [arg-type]
# protocols.py:24: note: Following member(s) of "Publishable" have conflicts:
# protocols.py:24: note:     Expected:
# protocols.py:24: note:         def publish(self, topic: str) -> Any
# protocols.py:24: note:     Got:
# protocols.py:24: note:         def publish(self) -> Any
# Found 1 error in 1 file (checked 1 source file)

But, if I explicitly subtype SupportsPublish, mypy does not report a type error:

...
@dataclass
class Publishable(SupportsPublish):
...

# ✗ mypy protocols.py
# Success: no issues found in 1 source file

Based on this blurb from the PEP, I expected the type checker to find the function signature mismatch:

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”. In addition, type checkers can statically verify that the class actually implements the protocol correctly:

This is my environment:


> mypy --version
mypy 1.3.0 (compiled: yes)
> python --version
Python 3.9.17

I expected mypy to point out the function signature mismatch.

like image 931
Andrew Stewart Avatar asked Jun 28 '26 20:06

Andrew Stewart


2 Answers

I just want to point out, as was established in the comments, it is not in fact true that if you explicitly subtype SupportsPublish, mypy does not report a type error.

The problem is that you weren't type annotating your method, which essentially tells mypy "don't check this".

If you do, for example:

from dataclasses import dataclass
from typing import Protocol

class SupportsPublish(Protocol):
    def publish(self, topic: str) -> None:
        ...

def publish(m: SupportsPublish):
    m.publish("topic")

@dataclass
class Publishable:
    foo: str = "bar"

    def publish(self) -> None:
        print(self)

Then mypy will complain:

(py311) Juans-MBP:~ juan$ mypy foo.py
foo.py:18: error: Argument 1 to "publish" has incompatible type "Publishable"; expected "SupportsPublish"  [arg-type]
foo.py:18: note: Following member(s) of "Publishable" have conflicts:
foo.py:18: note:     Expected:
foo.py:18: note:         def publish(self, topic: str) -> None
foo.py:18: note:     Got:
foo.py:18: note:         def publish(self) -> None
Found 1 error in 1 file (checked 1 source file)

Because this is a requirement of just regular subclassing with method overriding.

If you aren't going to run mypy with full --strict mode, at least somehow (through how it is invoked or by using a mypy.ini) make sure you have --disallow-untyped-defs or --disallow-untyped-calls

like image 178
juanpa.arrivillaga Avatar answered Jul 01 '26 10:07

juanpa.arrivillaga


The publish method must have the same signature in the class as in the protocol to conform to the protocol. Also, you shouldn't subclass protocols except possibly to create other protocols; they are implicit in the sense that a class (or other thing) "conforms to" the protocol if it matches a superset of the protocol definition, not by explicitly linking to it:

A concrete type X is a subtype of protocol P if and only if X implements all protocol members of P with compatible types.

In other words, for a class to be a subtype of SupportsPublish it is necessary and sufficient for it to have a method with signature publish(self, topic: str) -> None.

Here's a version which supports mypy --strict:

from abc import abstractmethod
from dataclasses import dataclass
from typing import Protocol

class SupportsPublish(Protocol):
    @abstractmethod
    def publish(self, topic: str) -> None:
        ...

def publish(m: SupportsPublish) -> None:
    m.publish("topic")

@dataclass
class Publishable:
    foo: str = "bar"

    def publish(self, topic: str) -> None:
        print(topic)

publish(Publishable())
like image 28
l0b0 Avatar answered Jul 01 '26 09:07

l0b0