Description
- Are you reporting a bug, or opening a feature request? bug
- Please insert below the code you are checking with mypy,
or a mock-up repro if the source is private. We would appreciate
if you try to simplify your case to a minimal repro.
from typing import overload
class Expr: ...
class Foo:
@overload # type: ignore
def __eq__(self, other: 'Foo') -> 'Expr': ...
@overload
def __eq__(self, other: str) -> 'Expr': ...
foo: Foo
reveal_type(foo == 1.0)- What is the actual behavior/output?
main.pyi:17: error: Revealed type is 'builtins.bool'- What is the behavior/output you expect?
main.pyi:17: error: No overload variant of "__eq__" of "Foo" matches argument type "float"
main.pyi:17: note: Possible overload variants:
main.pyi:17: note: def __eq__(self, Foo) -> Expr
main.pyi:17: note: def __eq__(self, str) -> Expr
Since Foo.__eq__ exists, it will be called for all equality comparisons at runtime, and it will not fall back to float.__eq__ unless it returns NotImplemented. Since none of the overload variants do that, it should never happen and this comparison should be a type error.
My current workaround is to include a Any overload that returns NoReturn, but that's not ideal since it defers the error at the call site to a "if I get lucky it will hit an error downstream" situation. Some of that could be addressed with more precise NoReturn semantics, but as it stands right now there are still some discussions/issues surrounding that, and it also feels clunky to have to explicitly include an overload for type errors.
In any case, I think special method overload resolution needs some fine tuning, and it should not mix-and-match the left/right operand's methods unless an overload variant is allowed to return type(NotImplemented) (which, btw, needs some type tightening in typeshed, but I'll file that over there).
- What are the versions of mypy and Python you are using?
0.710+dev.bb7dbd5afd84656a62311e5f69a1cef6d06466bc - Do you see the same issue after installing mypy from Git master?
yes
Activity
Michael0x2a commentedon Apr 22, 2019
I think the root cause here is actually due to the
# type: ignore. In short,object.__eq__(...)is supposed to be able to accept any value and return any bools. (The special operator methods are also assumed to be capable of returningNotImplementedat any arbitrary point.)So, when the subclass doesn't use the same signature (doesn't respect Liskov), you get inconsistencies like this.
Unfortunately, I'm not sure if there's a good workaround for this: the signature for
object.__eq__was defined in a way that makes conventional definitions easy, but doesn't leave a lot of wiggle room for these sorts of DSLs.Probably there's a better way of handling this, but it's somewhat unclear to me what that way would be. For example, adjusting mypy to do this fallback behavior only when
NotImplementedTypeis returned probably won't be sufficient: we'd also almost certainly need to adjust the signature ofobject.__eq__in typeshed in some way. Finding a way of doing this that doesn't disrupt existing user code (or at least minimizes it) is likely non-trivial.llchan commentedon Apr 22, 2019
That's what I thought initially too, that the type ignore was causing something funny to happen with overload resolution where it would inherit a partial type signature from the base class. However, I did some testing and I think the example below makes it more clear that it's using the right operand's
__eq__:Seems to me that these comparisons try overloads from the left and then try overloads from the right, rather than checking for the existence of the special method on the left and only looking at the right if the matching left signature returns NotImplemented.
gvanrossum commentedon Apr 22, 2019
NotImplementedis defined as having typeAny. This is intentional, otherwise the signature of nearly every binary dunder method would have to be an ugly union withNotImplemented. Therefore it can be returned from anything without violating the type system. So mypy assumes that there is always a possibility that the left operand returnsNotImplemented, and hence it always takes the right operand's type into account.Now, I do understand there's a real problem here -- mypy and typeshed currently aren't built to support overloading
__eq__to return a non-bool, because it is defined so in typeshed for classobject.I don't see an easy way out -- in particular because in general
==can be used to compare any two objects, and is implicitly used by other operations likeinand dict key access. Sorry.Michael0x2a commentedon Apr 22, 2019
The reason it's doing this is because mypy is assuming that the left operand overrode
__eq__correctly. My point is that since the code violates this assumption, the resulting behavior of mypy is no longer guaranteed to be correct.llchan commentedon Apr 22, 2019
I agree that this will not be an easy ticket to solve, and I expect it will take some time to figure out how to address this with a reasonable upgrade path. That said, I think it's important for a type checker to value correctness over brevity, and that's especially true in this case since it's not a self-contained false positive that can be hidden away by a
# type: ignore. The incorrectly-inferred type can propagate and lead to uncaught type issues downstream, and we should move towards fixing that, even if it's hard and takes a while to figure out.Now before we go further, I want to make sure I have my understanding correct:
NotImplementedis an instance ofAnyto keep binary dunder signatures clean__eq__method, and if it exists it assumes it may always returnNotImplemented, since that's an instance ofAny__eq__to be invoked, and its signature(s) are used following the left operand's.Is that a reasonable summary?
llchan commentedon Apr 23, 2019
For what it's worth, pyre and pyright both emit an error:
pyre:
pyright:
ilevkivskyi commentedon Apr 25, 2019
I am with @Michael0x2a here. After an ignored error, we try to avoid further false negatives on the best effort basis (unlike subsequent false positives, these should be always avoided if the original error is ignored). I am therefore proposing to close this issue.
llchan commentedon Apr 30, 2019
I understand the current state of things (type ignore causing assumptions to be broken), and I can understand decreasing the issue priority since it's likely to take a bit of reworking to fix. However, I don't think this should be closed, since it's a real bug with a reliable repro and no solution. Maybe stick a help-wanted label on it, to indicate you guys arent actively tackling it, but it's still an unsolved issue?
I can try to poke around. Don't want to be one of those people whole files issues but doesnt contribute PRs. The mypy codebase is a lot to take in though, so don't hold your breath.
Michael0x2a commentedon May 1, 2019
I have no opinion on whether this issue should be open or closed, but I do want to clarify that I don't think this is a bug: mypy detected the root error and your code samples silenced it. And as Ivan said, mypy operates on a best-effort principle once an error is silenced and doesn't guarantee it'll identify subsequent "downstream" errors. In that regard, everything is behaving as expected.
Rather, I'd characterize this as in part a usability issue and in part a feature request:
While mypy did correctly detect that the code was not type safe, I do agree it'd be nice if it inferred types using the incorrect definition of
__eq__instead of the expected definition. It wouldn't change correctness (since the error was already identified), but it would probably improve usability in cases like this.A little more broadly, this is a feature request. Basically, mypy assumes that any code defining a custom operator methods are following Python's standard data model (
__eq__returns bool, operator methods return NotImplemented when given objects they can't handle...). This assumption makes sense since most objects follow this model and builtins such as dict or set assume that they do. But it would be nice if we could figure out a way to broaden the type system so that it can also accommodate non-standard operator definitions. It's not obvious to anybody how to actually do this though.Actually, I suppose there is one thing here I'd count as a bug: mypy doesn't enforce that operator methods correctly use NotImplemented. If mypy expects operator methods to follow Python's standard data model, I think it also makes sense to try and actually enforce this.
llchan commentedon May 1, 2019
Right, I think that's sort of the crux of the short-term issue: mypy always implies
Union[<return type>, NotImplemented]on these methods rather than implying it once at theobjectlevel. If the user decides to override it with a type ignore, it should respect the user's return type and not inject a NotImplemented if it's not possible for that to occur. The user should includeNotImplementedType(akaAny, for the time being) in their return union if that's a possible outcome.Longer-term, I agree this is pretty nontrivial feature request to broaden the special methods so they support a wider range of use cases. The docs say
so
object's subclasses should be allowed to return anything (whether automatic or via a special decorator or marker), and specific subclass implementations should overload where appropriate to returnbool/NotImplemented. A lot of painstaking work to get that into typeshed though, so I understand the reluctance :)