mypy error, overload with Union/Optional, “Overloaded function signatures 1 and 2 overlap with incompatible return types”

喜夏-厌秋 提交于 2019-12-05 04:57:19

There is not going to be a particularly clean way of solving this, I'm afraid -- at least, none that I'm personally aware of. As you've observed, your type signatures contain a fundamental ambiguity that mypy will not allow: if you attempt to call Add with an argument of type None, mypy will fundamentally not be able to deduce which of the given overload variants matches.

For more discussion about this, see the mypy docs on checking overload invariants -- search for the paragraph discussing "inherently unsafely overlapping variants" and start reading from there.


However, we can wiggle free in this specific case by spelling out the overloads to more precisely match the actual runtime behavior. In particular, we have this nice property that if either argument is 'None', we must also return None. If we encode this, mypy ends up being satisfied:

@overload
def Add(this: None, that: None) -> None:
    ...
@overload
def Add(this: Foo, that: None) -> None:
    ...
@overload
def Add(this: Bar, that: None) -> None:
    ...
@overload
def Add(this: Baz, that: None) -> None:
    ...
@overload
def Add(this: None, that: Foo) -> None:
    ...
@overload
def Add(this: None, that: Bar) -> None:
    ...
@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> Baz:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

x: Optional[Baz]
y: Optional[Bar]

reveal_type(Add(x, y))  # Revealed type is 'Union[Baz, None]'

The fact that this works may initially seem surprising -- after all, we pass in an argument of type Optional[...] and yet none of the overloads contain that type!

What mypy is doing here is informally called "union math" -- it basically observes that x and y are both unions of type Union[Baz, None] and Union[Bar, None] respectively, and tries doing the spiritual equivalent of a nested for loop to check every single possible combination of these unions. So, in this case it checks for an overload variant that matches (Baz, Bar), (Baz, None), (None, Bar), and (None, None) and gets back return values of types Baz, None, None, and None respectively.

The final return type is then the union of these values: Union[Baz, None, None, None]. This simplifies to Union[Baz, None], which is the desired return type.


The main downside of this solution of course, is that it's extremely verbose -- perhaps to an unbearable extent, depending on how many of these helper functions you have and how pervasive this "we could maybe return None" problem is in your codebase.

If this is the case, something you could possibly do is declare "bankruptcy" on the pervasiveness of 'None' throughout your codebase and start running mypy with "strict optional" mode disabled.

In short, if you run mypy with the --no-strict-optional flag, you're instructing mypy to assume that 'None' is a valid member of every class. This is the same as how Java assumes that 'null' is a valid member of every type. (Well, every non-primitive type, but whatever).

This weakens the type safety of your code (sometimes dramatically), but it will let you simplify your code to look like this:

class Foo:
    value: int
    def __init__(self, value: int) -> None:
        self.value = value

    # Note: with strict-optional disabled, returning 'Foo' vs
    # 'Optional[Foo]' means the same thing
    def __add__(self, other: 'Foo') -> Foo:
        result = self.value - other.value
        if result > 42:
            return None
        else:
            return Foo(result)


@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> Baz:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

x: Optional[Baz]
y: Optional[Bar]

reveal_type(Add(x, y))  # Revealed type is 'Baz'

Strictly speaking, the overload checks ought to report the "unsafely overlapping" error for the same reason they did back when strict-optional was enabled. However, if we did so, overloads would be completely unusable when strict-optional is disabled: so mypy deliberately weakens the checks here and ignores that particular error case.

The main disadvantage of this mode is that you're now forced to do more runtime checking. If you receive back some value of type Baz, it might actually be None -- similar to how any object reference in Java could actually be null.

This might potentially be an ok tradeoff in your case, since you're already scattering these types of runtime checks everywhere.

If you subscribe to the "null was a billion-dollar mistake" school of thought and would like to live in the strict-optional world, one technique you can use is to progressively re-enable strict-optional in select parts of your codebase by using the mypy config file.

Basically, you can configure many (though not all) mypy options on a per-module basis via the config file, which can be pretty handy if you're attempting to add types to a pre-existing codebase and find that transitioning all at once is simply intractable. Start with loose global settings, then gradually make them stricter and stricter over time.


If both of these options feels too extreme (e.g. you don't want to add the verbose signature from above everywhere, but also don't want to give up strict optional), the final option you could do is to just simply silence the error by adding a # type: ignore to every line mypy reports an "unsafely overlapping types" error on.

This is also defeat in a way, but possibly a more localized one. Even typeshed, the repository of type hints for the standard library, contains a few scattered # type: ignore comments here and there for certain functions that are simply inexpressible using PEP 484 types.

Whether or not this is a solution you're ok will depend on your particular circumstance. If you analyze your codebase, and think that the potential unsafeness is something you're ok with ignoring, maybe this might be the simplest way forward.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!