Description
Some time ago I encountered another issue in production code that we may need to solve before implementing strict None
type checking (#357). Consider a function like this:
def convert(s: Optional[str]) -> Optional[int]:
if s is None:
return None
return int(s)
Now code like this would be rejected according to PEP 484 rules, as the return type unconditionally includes None
:
convert('2') + 1 # Error: can't add None and int
We could use overloading or single dispatch to model this, but this would involve pretty major refactoring for existing code and probably some performance overhead.
Alternatively, we could model this as a generic function, but currently there's no syntax to model the above case. Here is a strawman proposal:
from typing import OptionalTypeVar
Maybe = OptionalTypeVar('Maybe')
def convert(s: Maybe[str]) -> Maybe[int]:
if s is None:
return None
return int(s)
OptionalTypeVar
would work a bit like TypeVar
with values, such as AnyStr
, with a slight twist: the first value would always be None
, and the second value is the index value, and this could have multiple different values in different positions. So Maybe[T]
would be replaced with None
or T
, but all instances of Maybe
would get either the first or second value everywhere in lock step fashion. The above function would thus be equivalent to this from type checking perspective (assuming we had overloading):
@overload
def convert(s: None) -> None:
if s is None:
return None
return int(s)
@overload
def convert(s: str) -> int:
if s is None:
return None
return int(s)
We could generalize this to arbitrary collections of types. I can't come up with a good syntax, but here is the first thing that comes to mind:
from typing import Alternate
def convert(s: Alternate[None, str]) -> Alternate[None, int]:
... # Same as above
The semantics would be the same as above. Now we could define a function that maps ints to string and vice versa:
from typing import Alternate
def switch(s: Alternate[int, str]) -> Alternate[str, int]:
...
Here the issue is that we can't have multiple alternate variables that could vary independently. Maybe there would be a way to define additional Alternate
variants with different names, similar to TypeVar
.
The latter could also deal with True
and False
polymorphism assuming they would be considered subtypes of bool
. Here is an example that is similar to some examples in the std library (in subprocess
, I think):
class Stream(Generic[Alternate[str, bytes]]):
def __init__(self, unicode: Alternate[True, False]) -> None: ...
def stream(self) -> IO[Alternate[str, bytes]]: ...
Interestingly, we could replace AnyStr
with this definition instead of using TypeVar
:
AnyStr = Alternate[str, bytes]