I want to do this:
import typing
import dataclasses
class ConfigProtocol(typing.Protocol):
a: str
@dataclasses.dataclass
class Config:
a: str
class HasConfigProtocol(typing.Protocol):
config: ConfigProtocol
@dataclasses.dataclass
class HasConfig:
config: Config
def accepts_hasconfig(h: HasConfigProtocol):
pass
accepts_hasconfig(HasConfig(config=Config(a="")))
But in pylance it reports:
Argument of type "Position" cannot be assigned to parameter "template_position" of type "PositionProtocol" in function "__init__"
"Position" is incompatible with protocol "PositionProtocol"
"config" is invariant because it is mutable
"config" is an incompatible type
"Configuration" is incompatible with protocol "ConfigurationProtocol"
To get this to work do I need to freeze my dataclasses and provide read only access in my protocols like so?
class ConfigProtocol(typing.Protocol):
@property
def a(self) -> str:
...
@dataclasses.dataclass(frozen=True)
class Config:
a: str
class HasConfigProtocol(typing.Protocol):
@property
def config(self) -> ConfigProtocol:
...
@dataclasses.dataclass(frozen=True)
class HasConfig:
config: Config
def accepts_hasconfig(h: HasConfigProtocol):
pass
accepts_hasconfig(HasConfig(config=Config(a="")))
Long term this is the code in one module, module_a which is stand alone. There is another module_b which also exists, has similar code and is stand alone. I want to use instances of HasConfig from module_a in module_b. Because the modules do not know about eachother, I am including the same protocol definition in each of them.
To reuse protocols across modules and have instances work for both of them, the protocol definitions must be the same, and the Protocol definitions only must use @property definitions.
The issue is that HasConfig
class accepts Config
and not the ConfigProtocol
for attribute config
. This makes HasConfig
incompatible with HasConfigProtocol
, if and only if HasConfigProtocol.config
is mutable.
The reason for this is if the config
attribute is mutable, then someone working with HasConfigProtocol
could expect to set the config
attribute to anything that conforms to the ConfigProtocol
and not just Config
. This other class will conform to the ConfigProtcol
interface, but it might not conform to the interface of Config
, and so errors could be introduced. Given your example only has one attribute it is hard to see what the issue is. Perhaps a worked example with multiple attributes could better indicate the pitfalls that the type checker is trying to save you from.
class FooP(Protocol):
number: int
@dataclass
class Foo:
number: int
other_attr: float
@dataclass
class AnotherFooImpl:
number: int
some_ints: list[int]
class BarP(Protocol):
foo: FooP
@dataclass
class Bar:
foo: Foo
bar: Bar = Bar(foo=Foo(number=0, other_attr=1.0))
assert bar.foo.other_attr == 1.0
bar_p: BarP = bar
bar_p.foo = AnotherFooImpl(0, []) # valid according to BarP, but not to
# according to Bar
assert bar.foo.other_attr == 1.0 # AttributeError! AnotherFooImpl has no
# attribute called other_attr
The fix for the static type checker is simple. Change your HasConfig
implementation such that is accepts ConfigProtocol
instead of Config
. ie.
@dataclasses.dataclass
class HasConfig:
config: ConfigProtocol
# ^^^^^^^^-- new bit