Reputation: 1603
Consider a Python protocol attribute which is also annotated with a protocol. I found in that case, both mypy and Pyright report an error even when my custom datatype follows the nested protocol. For example in the code below Outer
follows the HasHasA
protocol in that it has hasa: HasA
because Inner
follows HasA
protocol.
from dataclasses import dataclass
from typing import Protocol
class HasA(Protocol):
a: int
class HasHasA(Protocol):
hasa: HasA
@dataclass
class Inner:
a: int
@dataclass
class Outer:
hasa: Inner
def func(b: HasHasA): ...
o = Outer(Inner(0))
func(o)
However, mypy shows the following error.
nested_protocol.py:22: error: Argument 1 to "func" has incompatible type "Outer"; expected "HasHasA" [arg-type]
nested_protocol.py:22: note: Following member(s) of "Outer" have conflicts:
nested_protocol.py:22: note: hasa: expected "HasA", got "Inner"
What's wrong with my code?
Upvotes: 7
Views: 741
Reputation: 3608
There's an issue on GitHub which is almost exactly the same as your example. I think the motivating case on the mypy
docs explains quite well why this is illegal. Bringing a structural analogy to your example, let's fill in an implementation for func
and tweak Inner
slightly:
def func(b: HasHasA) -> None:
b.hasa.a += 100 - 100
@dataclass
class Inner:
a: bool
o = Outer(Inner(bool(0)))
func(o)
if o.hasa.a is False:
print("Oh no! This is still False!")
else:
print("This is true now!")
This is of course a contrived example, but it shows that if the type-checker didn't warn you against this, the inner protocol can type-widen the inner type and perform value mutation, and you may silently perform type-unsafe operations.
As suggested by the mypy
documentation, the solution is to make the outer protocol's variable read-only:
class HasHasA(Protocol):
@property
def hasa(self) -> HasA:
...
Upvotes: 6