Search code examples
pythonprotocolsmypy

mpy gives keeps raising error because of Protocol


Im using pytest framework with mypy and when writing fixtures I created Protocols to mimic return types. (I could not import it directly because of env instantiation)

This example greatly simplifies my troubles:

 from typing_extensions import Protocol

class LoadServiceType(Protocol):
    z: int

class LoadService:
    z: int

#This works as expected
load_serivce: LoadServiceType = LoadService()

# But now I dont know why this does not work
class FooType(Protocol):
    load_service: LoadServiceType
    
class Boo:
    x:int
    load_service:LoadService
    bb: int
    
    
cc: FooType = Boo()

Basically I want to use my FooType as a return type because of import issues. Is there any solution ?


Solution

  • This problem can be represented by the following minimal example:

    from dataclasses import dataclass
    from typing import Protocol
    
    
    @dataclass
    class Fooish:
        bar: int
    
    
    class FooLike(Protocol):
        bar: float
    
    
    foo: FooLike = Fooish(bar=321)
    

    mypy raises the following error:

    foo.py:18: error: Incompatible types in assignment (expression has type "Fooish", variable has type "FooLike")  [assignment]
    foo.py:18: note: Following member(s) of "Fooish" have conflicts:
    foo.py:18: note:     bar: expected "float", got "int"
    

    The FooLike protocol represents a set of objects which have a writable float member named bar. The Fooish class' bar member is an int and does not accept float values to be assigned, thus is incompatible.

    According to foo: FooLike, the following should be valid, but isn't:

    foo: FooLike = Fooish(bar=321)
    foo.bar = 21.37
    

    To fix the compatibility issue, one should define the variable as read-only via a @property:

    from dataclasses import dataclass
    from typing import Protocol
    
    
    @dataclass
    class Fooish:
        bar: int
    
    
    class FooLike(Protocol):
        @property
        def bar(self) -> float:
            ...
    
    
    foo: FooLike = Fooish(bar=321)
    

    So, going back to your original example, the following snippet works as expected:

    from typing_extensions import Protocol
    
    
    class LoadServiceType(Protocol):
        z: int
    
    
    class LoadService:
        z: int
    
    
    load_serivce: LoadServiceType = LoadService()
    
    
    class FooType(Protocol):
        @property
        def load_service(self) -> LoadServiceType:
            ...
    
    
    class Boo:
        x: int
        load_service: LoadService
        bb: int
    
    
    cc: FooType = Boo()