Search code examples
pythonmypypython-typingpyright

Usage of nested protocol (member of protocol is also a protocol)


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?


Solution

  • 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:
            ...