So, let's start with an example. Suppose we have several types that can be combined together, let's say we are using __add__
to implement this. Unfortunately, due to circumstances beyond our control, everything has to be "nullable", so we are forced to use Optional
everywhere.
from typing import Optional, List, overload
class Foo:
value: int
def __init__(self, value: int) -> None:
self.value = value
def __add__(self, other: 'Foo') -> 'Optional[Foo]':
result = self.value - other.value
if result > 42:
return None
else:
return Foo(result)
class Bar:
value: str
def __init__(self, value: str) -> None:
self.value = value
def __add__(self, other: 'Bar') -> 'Optional[Bar]':
if len(self.value) + len(other.value) > 42:
return None
else:
return Bar(self.value + other.value)
class Baz:
value: List[str]
def __init__(self, value:List[str]) -> None:
self.value = value
def __add__(self, other: 'Bar') -> 'Optional[Baz]':
if len(self.value) + 1 > 42:
return None
else:
return Baz([*self.value, other.value])
@overload
def Add(this: Optional[Foo], that: Optional[Foo]) -> Optional[Foo]:
...
@overload
def Add(this: Optional[Bar], that: Optional[Bar]) -> Optional[Bar]:
...
@overload
def Add(this: Optional[Baz], that: Optional[Bar]) -> Optional[Baz]:
...
def Add(this, that):
if this is None or that is None:
return None
else:
return this + that
We want utility function that does null-checking for us, but can generically handle "combinable" types. Most of the types can only be combined with themselves, and just to be more true to my actual use-case, let's say one type combines with the other. I would have hoped that the overload
decorator could have helped here, however mypy complains:
mcve4.py:35: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
mcve4.py:35: error: Overloaded function signatures 1 and 3 overlap with incompatible return types
mcve4.py:38: error: Overloaded function signatures 2 and 3 overlap with incompatible return types
Using mypy version: mypy 0.641
Note, if I remove Optional
madness, mypy doesn't complain. I can even keep one of them as optional!:
from typing import List, overload
class Foo:
value: int
def __init__(self, value: int) -> None:
self.value = value
def __add__(self, other: 'Foo') -> 'Foo':
result = self.value - other.value
return Foo(result)
class Bar:
value: str
def __init__(self, value: str) -> None:
self.value = value
def __add__(self, other: 'Bar') -> 'Bar':
return Bar(self.value + other.value)
class Baz:
value: List[str]
def __init__(self, value:List[str]) -> None:
self.value = value
def __add__(self, other: 'Bar') -> 'Optional[Baz]':
return Baz([*self.value, other.value])
@overload
def Add(this: Foo, that: Foo) -> Foo:
...
@overload
def Add(this: Bar, that: Bar) -> Bar:
...
@overload
def Add(this: Baz, that: Bar) -> 'Optional[Baz]':
...
def Add(this, that):
if this is None or that is None:
return None
else:
return this + that
This makes me suspect that the "overlap" is for NoneType, but feel like this should be resolvable, am I completely off base?
Edit
So, I'm really just flailing about here, but I suppose, when both argument are None
this is definitely ambiguous, I would have hoped that the following would resolve it:
@overload
def Add(this: None, that: None) -> None:
...
@overload
def Add(this: Optional[Foo], that: Optional[Foo]) -> Optional[Foo]:
...
@overload
def Add(this: Optional[Bar], that: Optional[Bar]) -> Optional[Bar]:
...
@overload
def Add(this: Optional[Baz], that: Optional[Bar]) -> Optional[Baz]:
...
def Add(this, that):
if this is None or that is None:
return None
else:
return this + that
But I am still getting:
mcve4.py:37: error: Overloaded function signatures 2 and 3 overlap with incompatible return types
mcve4.py:37: error: Overloaded function signatures 2 and 4 overlap with incompatible return types
mcve4.py:40: error: Overloaded function signatures 3 and 4 overlap with incompatible return types
Edit2 Going along the same garden-trail, I've tried the following:
@overload
def Add(this: None, that: None) -> None:
...
@overload
def Add(this: Foo, that: Optional[Foo]) -> Optional[Foo]:
...
@overload
def Add(this: Optional[Foo], that: Foo) -> Optional[Foo]:
...
@overload
def Add(this: Baz, that: Bar) -> Optional[Baz]:
...
@overload
def Add(this: Baz, that: Optional[Bar]) -> Optional[Baz]:
...
@overload
def Add(this: Optional[Baz], that: Bar) -> Optional[Baz]: # 6
...
@overload
def Add(this: Bar, that: Optional[Bar]) -> Optional[Bar]:
...
@overload
def Add(this: Optional[Bar], that: Bar) -> Optional[Bar]: # 8
...
def Add(this, that):
if this is None or that is None:
return None
else:
return this + that
I am now getting:
mcve4.py:49: error: Overloaded function signatures 6 and 8 overlap with incompatible return types
Which is starting to make sense to me, I think fundamentally what I am trying to do is unsafe/broken. I may have to just cut the gordian knot another way...