python/mypy

mypy incorrectly complains about incompatible type error

Opened this issue · 2 comments

Bug Report

mypy is incorrectly complaining that the arguments to a function have an incompatible type, when used in the context of a class attribute.

In the example below, we are creating a library of fields (StringField, IntegerField, etc.), to be used in a runtime schema validation library. In addition, we create a OneOfField that takes a list of sub-fields via the with_ function. Creating a OneOfField works fine by itself, but when assigned to a class attribute, mypy incorrectly complains about a type error.

from typing import Generic, TypeVar, Union, assert_type, cast, overload, Self

InputType = TypeVar("InputType")
OutputType = TypeVar("OutputType")


class Field(Generic[InputType, OutputType]):
    # We want both InputType and OutputType to be used in functions like the
    # ones below, so they can't be marked covariant or contravariant.
    def validate(self, input: Any) -> InputType:
        return cast(InputType, input)
    
    def render(self, input: InputType) -> OutputType:
        return cast(OutputType, input)
    
    def serialize(self, output: OutputType) -> str:
        return ""


class StringField(Field[str, str]):
    pass


class IntegerField(Field[int, int]):
    pass


class BooleanField(Field[bool, bool]):
    pass


def input_type(field: Field[InputType, OutputType]) -> InputType:
    return cast(InputType, field)


OneOfInputType = TypeVar("OneOfInputType")
OneOfOutputType = TypeVar("OneOfOutputType")

AInputType = TypeVar("AInputType")
AOutputType = TypeVar("AOutputType")

BInputType = TypeVar("BInputType")
BOutputType = TypeVar("BOutputType")

CInputType = TypeVar("CInputType")
COutputType = TypeVar("COutputType")


class OneOfField(
    Generic[OneOfInputType, OneOfOutputType],
    Field[OneOfInputType, OneOfOutputType],
):
    fields: list[Field]

    def __init__(self) -> None:
        super().__init__()
        self.fields = []

    @overload
    def with_(
        self,
        a: Field[AInputType, AOutputType],
        b: Field[BInputType, BOutputType],
    ) -> "OneOfField[Union[AInputType, BInputType], Union[AOutputType, BOutputType]]": ...

    @overload
    def with_(
        self,
        a: Field[AInputType, AOutputType],
        b: Field[BInputType, BOutputType],
        c: Field[CInputType, COutputType],
    ) -> "OneOfField[Union[AInputType, BInputType, CInputType], Union[AOutputType, BOutputType, COutputType]]": ...

    def with_(
        self,
        a: Field[AInputType, AOutputType],
        b: Field[BInputType, BOutputType],
        c: Field[CInputType, COutputType] | None = None,
    ) -> "OneOfField":
        if c is None:
            self.fields = [a, b]
        else:
            self.fields = [a, b, c]
        return self

    def return_self(self) -> Self:
        return self

# This works:
field = OneOfField().with_(StringField(), IntegerField())
assert_type(field, OneOfField[Union[str, int], Union[str, int]])
assert_type(input_type(field), Union[str, int])


# This doesn't work:
class MyModel:
    # error: Argument 1 to "with_" of "OneOfField" has incompatible type "StringField"; expected "Field[str | int, str | int]"  [arg-type]
    # error: Argument 2 to "with_" of "OneOfField" has incompatible type "IntegerField"; expected "Field[str | int, str | int]"  [arg-type]
    foo: str | int = input_type(OneOfField().with_(StringField(), IntegerField()))
    
# This works?
class MyModel2:
    foo: str | int = input_type(OneOfField().with_(StringField(), IntegerField()).return_self())

To Reproduce

https://gist.github.com/mypy-play/4619ce6d001d7c7c6994d2b6c912424a

This does not repro in Pyright: link

Expected Behavior

No errors.

Actual Behavior

mypy raises an invalid error saying the argument to with_ is invalid.

Your Environment

  • Mypy version used: 1.13.0
  • Mypy command-line flags: N/A
  • Mypy configuration options from mypy.ini (and other config files): N/A
  • Python version used: 3.13

Smaller repro with one typevar (also excluding potential self influence):

from typing import Any, Generic, TypeVar

_I = TypeVar("_I")

class Field(Generic[_I]): pass
class StringField(Field[str]): pass
class IntegerField(Field[int]): pass


_A = TypeVar("_A")
_B = TypeVar("_B")

class OneOfField(Field[_I]):
    def __init__(
        self: "OneOfField[_A | _B]",
        a: Field[_A],
        b: Field[_B],
    ) -> None:
        pass


field: OneOfField[str|int] = OneOfField(StringField(), IntegerField())  # \
    # E: Argument 1 to "OneOfField" has incompatible type "StringField"; expected "Field[str | int]"  [arg-type] \
    # E: Argument 2 to "OneOfField" has incompatible type "IntegerField"; expected "Field[str | int]"  [arg-type]

playground

Thanks for simplifying!