Migwell
Migwell

Reputation: 20127

Defining a generic parameter with a default value in Python

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

Answers (2)

Rob Percival
Rob Percival

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

Paweł Rubin
Paweł Rubin

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

Related Questions