Reputation: 20127
Let's say I have the following code:
from typing import Literal, TypeVar, Generic
T = TypeVar("T", bound=str)
class Foo(Generic[T]):
def foo(self, arg: T = "foo") -> T:
return arg
The idea is that I have a generic class with a concrete method that takes a string, but the child classes can define a more restrictive input than any string, for example this class will only accept "bar"
:
class Bar(Foo[Literal["bar"]]):
pass
However, the original code doesn't pass any type checker. Both mypy and pyright complain. The mypy error is:
test.py:6: error: Incompatible default for argument "arg" (default has type "str", argument has type "T")
I would have thought this would be okay, because T
has an upper bound which is a string. However I think the issue is that the default argument "foo" no longer makes sense if the child class accepts only "bar". How then can I define T
such that it must always allow the literal "foo"
, but the child class can also define additional strings it will accept?
Upvotes: 2
Views: 2591
Reputation: 58
The idea is that I have a generic class with a concrete method that takes a string, but the child classes can define a more restrictive input than any string
This is a violation of the Liskov Substitution Principle (LSP), so mypy and pyright are correct to refuse it. A child class needs to accept anything that the parent class accepts, otherwise you can't freely substitute a child class for the parent class. You can do the opposite though; a child class is allowed to be more permissive than the parent.
Upvotes: 0
Reputation: 3370
In addition to a suggestion from comments, consider using typing.overload
:
from typing import TYPE_CHECKING, Literal, TypeVar, Generic, overload
from typing_extensions import reveal_type
T = TypeVar("T", bound=str | Literal["foo"])
class Foo(Generic[T]):
@overload
def foo(self, arg: T) -> T:
...
@overload
def foo(self) -> Literal["foo"]:
...
def foo(self, arg: T | Literal["foo"] = "foo") -> T | Literal["foo"]:
return arg
class Bar(Foo[Literal["bar"]]):
pass
if TYPE_CHECKING:
reveal_type(Bar().foo("bar")) # Revealed type is "Literal['bar']"
reveal_type(Bar().foo()) # Revealed type is "Literal['foo']"
Upvotes: 3