Search code examples
pythonpython-typingmypy

Dict type variable on non generic class methods


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?


Solution

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