Skip to content

Type checking functions where return type is always None if an argument is None #885

Closed
@JukkaL

Description

@JukkaL

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]

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions