1

When a Generic class has a default value in an init parameter, MyPy complains about the possibility of a type mismatch even though the init parameter defines the type.

Consider the following minimal example

class IntOrStrReplicator(Generic[IntStrType]):
    def __init__(self, a: IntStrType) -> None:
        self.a = a
    def produce(self) -> IntStrType:
        return self.a

We expect the following

reveal_type(IntOrStrReplicator(4)) # IntOrStrReplicator[int]
reveal_type(IntOrStrReplicator("Hello")) # IntOrStrReplicator[str]
reveal_type(IntOrStrReplicator(4).produce()) # int
reveal_type(IntOrStrReplicator("Hello").produce()) # str

Sure enough, we get that if IntStrType is either

TypeVar("IntStrType", bound=Union[int, str])

or

TypeVar("IntStrType", int, str)

Now, all I want to add is a default value of a. For example def __init__(self, a: IntStrType = 4) -> None: Clearly there's no problem with creating one of these classes with a=4. However the line then complains. If the TypeVar is given as a Union, it says:

Incompatible default for argument "a" (default has type "int", argument has type "T1")

and if the TypeVar is given as two different options it says

Incompatible default for argument "a" (default has type "int", argument has type "str")
Josiah
  • 1,072
  • 6
  • 15

2 Answers2

2

When a Generic class has a default value in an init parameter, MyPy complains about the possibility of a type mismatch even though the init parameter defines the type.

The problem here is that the __init__ parameter does not define the type. You can do something like

x = IntOrStrReplicator[str]()

where the type variable is bound to str, but the default value is still 4.

Your code isn't saying that IntOrStrReplicator() defaults to IntOrStrReplicator[int](4). Your code is saying that both IntOrStrReplicator[int]() and IntOrStrReplicator[str]() use a default argument of 4. IntOrStrReplicator[str](4) a type error, which is why your default value is invalid.

I don't think there's any way to express what you seem to have intended.


In the absence of an explicit type argument, type checkers will attempt to infer the type, but even then, it doesn't just go by the constructor arguments. It also takes the context into account, so in the following code:

from typing import Generic, TypeVar

T = TypeVar('T')

class Foo(Generic[T]):
    def __init__(self, x: T):
        self.x = x

def f(x: Foo[int]): pass
def g(x: Foo[object]): pass

f(Foo(3))
g(Foo(3))

T is deduced as int for the first Foo(3) and object for the second Foo(3).

user2357112
  • 260,549
  • 28
  • 431
  • 505
2

As explained by user2357112, the problem with this is that __init__ only sometimes defines the type parameter, and that explicit types can fail.

To express defaults in this way, you would need to restrict the default option to the corresponding type. You can do this with overload with a restriction on self. For example

class IntOrStrReplicator(Generic[T1]):
    @overload
    def __init__(self: "IntOrStrReplicator[int]") -> None:
        # int variant, only, allows leaving the parameter implied
        ...

    @overload
    def __init__(self: "IntOrStrReplicator[T1]", a:T1) -> None:
        # supplying the parameter requires that the type match.
        ...

    def __init__(self, a = 4) -> None:
        # This function needs programmer care because `a` still isn't checked
        # but consumers of the class take the overload definitions.
        self.a: T1 = a
Josiah
  • 1,072
  • 6
  • 15