Consider a function like scipy.integrate.solve_ivp
. The parameter method
has 6 known values: “RK45”, “RK23”, “DOP853”, “Radau”, “BDF” and “LSODA”.
Annotating method: Literal["RK45", "RK23", "DOP853", "Radau", "BDF", "LSODA"]
is likely out of the question, since this would cause type errors whenever the argument is given dynamically.
On the other hand, it would be very desirable to flag usage when we know statically that the argument given is incorrect, such as method="RK44"
.
One can get close to the desired behavior with multiple overloads: Code sample in pyright playground
from typing import overload, Literal, LiteralString, Never
@overload
def solve_ivp(method: Literal["RK45", "RK23"]) -> None: ...
@overload
def solve_ivp(method: LiteralString) -> Never: ...
@overload
def solve_ivp(method: str) -> None: ...
def solve_ivp(method) -> None:
pass
arg = input()
solve_ivp(arg) # OK
solve_ivp("RK23") # OK
solve_ivp("RK44") # not OK
solve_ivp("RK45") # OK <- marked unreachable due to prior Never
Which could be improved further if something like the previously proposed typing.Error
is adopted. However, it would still be very verbose. There should be an easier way to express the type “one of these specific literal strings or any non-literal string”, ideally one that does not require the use of overloads. With a full algebra of types, this could be expressed as (str & Not[LiteralString]) | Literal["RK45", "RK23"]
, but this seems far away.
As an alternative, without introducing additional types, would be to specify that unions of literal strings with str
should be interpreted this way, for instance letting Literal["RK45", "RK23"] | str
mean “the literal strings ‘RK45’ or ‘RK23’ or any non-literal string”. Of course, it would be very understandable if there is little appetite for such additional special casing.