On the following code:
from typing import Generic
from typing import TypeVar
from typing import reveal_type
T = TypeVar('T')
class Field(Generic[T]):
"""A field definition with a default value."""
def __init__(self, default_value: T):
self.default_value = default_value
class FieldDataCollection:
"""A collection of field values."""
def __init__(self, value_per_field: dict[Field[T], T]) -> None:
self._value_per_field = value_per_field
def get_field_value(self, field: Field[T]) -> T:
"""Return the field value if in the collection or the field default value."""
return self._value_per_field.get(field, field.default_value)
if __name__ == '__main__':
foo = Field(1)
value = FieldDataCollection({foo: 2}).get_field_value(foo)
reveal_type(value)
I get the following errors from mypy with 1.11.0 (but not with previous versions) running in strict mode:
error: Incompatible return value type (got "T@__init__", expected "T@get_field_value") [return-value]
error: Argument 1 to "get" of "dict" has incompatible type "Field[T@get_field_value]"; expected "Field[T@__init__]" [arg-type]
error: Argument 2 to "get" of "dict" has incompatible type "T@get_field_value"; expected "T@__init__" [arg-type]
note: Revealed type is "builtins.int"
From what I understand of these errors mypy complains that the type inferred in the __init__
method is somehow different from the type in the get_field_value
method.
I tried to use a different type variable for the get_field_value
:
T = TypeVar("T")
Tfield = TypeVar("Tfield")
...
class FieldDataCollection:
...
def get_field_value(self, field: Field[Tfield]) -> Tfield:
...
But mypy seems to catch the difference and complains in the same fashion:
error: Incompatible return value type (got "T", expected "Tfield") [return-value]
error: Argument 1 to "get" of "dict" has incompatible type "Field[Tfield]"; expected "Field[T]" [arg-type]
error: Argument 2 to "get" of "dict" has incompatible type "Tfield"; expected "T" [arg-type]
note: Revealed type is "builtins.int"
I tried using Mapping
or MutableMapping
instead of dict
which works fine but then I cannot use methods of dict
like copy
.
Do you think it is a regression on mypy side?
Otherwise, do you have any idea on how to annotate the FieldDataCollection
class properly?
This seems to actually be a scoping issue, the type T in FieldDataCollection
is not defined, so the interpretation is different, and in this case not the interpretation you were looking for. Adding Generic[T]
to FieldDataCollection
resolves this for mypy version 1.11.0, as it ensures the type used in __init__
must be the same as that used in get_field_value
.
Also, as specified in the comments, to ensure both Field[str]
and Field[int]
can be used in FieldDataCollection
we must make T covariant:
from typing import Generic
from typing import TypeVar
from typing import reveal_type
T = TypeVar('T', covariant=True)
class Field(Generic[T]):
"""A field definition with a default value."""
def __init__(self, default_value: T):
self.default_value = default_value
class FieldDataCollection(Generic[T]):
"""A collection of field values."""
def __init__(self, value_per_field: dict[Field[T], T]) -> None:
self._value_per_field = value_per_field
def get_field_value(self, field: Field[T]) -> T:
"""Return the field value if in the collection or the field default value."""
return self._value_per_field.get(field, field.default_value)
if __name__ == '__main__':
foo = Field(1)
foo_str = Field("test")
value = FieldDataCollection({foo: 2, foo_str: "v"}).get_field_value(foo)
reveal_type(value)
Hope this helps!
Update: On reviewing the earlier comments, I've expanded it to allow for the multiple key types.