You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Specify what happens for binary operators (e.g. __add__) #1548
There's a subtle dance around __add__, __radd__, and NotImplemented. While most of the rules are in the language spec, I think we haven't specified carefully what the signature should be of e.g. the __add__ method, especially when annotations are inline. (At least, I didn't find any mention of NotImplemented in the spec.)
In typeshed we seem to ignore the "overload" __add__: (Any) -> Literal[NotImplemented] (to be loose with notation), but what do typecheckers do when they see a concrete __add__ or __eq__ implementation that can return NotImplemented?
Those are not the exact rules. Sometimes __radd__ is called first (the rule is so complicated I don't recall offhand if that can be determined statically or not). And when both return NotImplemented, the interpreter issues the exception.
Anyway, we might still consider this, given that the convention in typeshed appears to be not to show NotImplemented. (This would only be a problem for direct callers of these overloads, which must be very rare.)
I am aware of the issues around Literal[NotImplemented], I just used as a shorthand (if it did work, it would be ideal :-).
Some thinking about how we could make the behavior more useful. Are there plausible user mistakes that type checkers currently can't catch, but that could be caught if we came up with a better set of rules? Conversely, are there patterns that work at runtime that type checkers currently do not allow?
I'm currently trying to improve Mypy's handling of NotImplemented a bit. Mypy (and, I think, Pyright) do not care much about it and so miss the following simple error:
If so, wouldn't there be much gained by letting type checkers analyse if a (in-place) binary special method's return type annotation contains NotImplementedType? Then,
could be understood precisely as the type hints suggest.
As NotImplementedType is available since Python 3.10 (and Python 3.9 reaches its end-of-life), type checkers would not need to implement None-like handling for NotImplemented or support Literal[NotImplemented].
Gradually adding NotImplementedType to return type hints would hopefully work relatively smoothly. A few experiments with Mypy:
For Pyright, the revealed types are a little different, but at least D1() + D1()) + 1.0 causes no wrong false positive (I am not familiar with Pyright and so do not know much about Unknown):
Type checkers could, of course, implement a (transition) option that lets them continue to assume that all (in-place) binary special methods eventually return NotImplemented.
(This would rely on interpreting overloads in declaration order, though. As I understand it, that's not necessarily true anymore, though.)
Your suggestion of interpreting def __add__(self, o: A1) -> float | NotImplementedType: like the explicit annotations above would necessitate further special handling in this case. It's also unclear what an annotation like def __add__(self, o: A1 | int) -> float | str | NotImplementedType: would mean.
Yes, my suggestion to allow "implicit overloads" would require some special casing in type checkers, but it would reduce code repetition (__add__, __radd__, __sub__...). And the binary special methods already require much special handling. Maybe a compromise could be to allow "implicit overloads" only for plain methods (without "normal overloads"). But I have no strong opinion here; I think your suggestion to require "explicit overloads" is equally fine.
Maybe one could write a helpful decorator for this purpose?
Yes, my suggestion to allow "implicit overloads" would require some special casing in type checkers, but it would reduce code repetition (add, radd, sub...).
My main problem is not the special casing in type checkers, but the special casing in user's minds. Users need to know about it, and it's another special case for a not super common feature.
Activity
superbobry commentedon Dec 21, 2023
I think type checkers can implement the semantics of
NotImplementedwithout actually requiring it in the types. Specifically, givena + b,type(a).__add__(b)type checks, i.e.type(a)has an overload expectingb, -- use that,type(b).__radd__(a).Note also that
Literal[NotImplemented]is not permitted becauseNotImplementedis not an enum nor a literal syntactically;NotImplementedTypecould be used to describe instances ofNotImplemented, but it is not currently available from pure python AFAICT;NotImplementedsimilarly toNonein thatNotImplementedin a type annotation meansNotImplementedType.gvanrossum commentedon Dec 24, 2023
Those are not the exact rules. Sometimes
__radd__is called first (the rule is so complicated I don't recall offhand if that can be determined statically or not). And when both returnNotImplemented, the interpreter issues the exception.Anyway, we might still consider this, given that the convention in typeshed appears to be not to show
NotImplemented. (This would only be a problem for direct callers of these overloads, which must be very rare.)I am aware of the issues around
Literal[NotImplemented], I just used as a shorthand (if it did work, it would be ideal :-).JelleZijlstra commentedon Jan 2, 2024
NotImplemented is generally just ignored by the type system; in typeshed we treat it as being a subclass of Any
https://github.com/python/typeshed/blob/d4daff337fc8ac122a10cff21ea2e7c9f3a30a6d/stdlib/builtins.pyi#L1261
I think to move forward here we'd need:
tyralla commentedon Oct 23, 2025
I'm currently trying to improve Mypy's handling of
NotImplementeda bit. Mypy (and, I think, Pyright) do not care much about it and so miss the following simple error:It is not clear to me where the type checkers fail. According to the documentation,
NotImplemented...should be returned by the binary special methods ... to indicate that the operation is not implemented with respect to the other type. This sounds more like the implementation ofA2is wrong. However, the documentation also says thatNotImplementedmay be returned by the in-place binary special methods ... for the same purpose. So here it is clear that returningNotImplementedis optional. I guess it would be more comprehensible to let type checkers handle both cases identically and to understand the should in the first statement also as a may.If so, wouldn't there be much gained by letting type checkers analyse if a (in-place) binary special method's return type annotation contains NotImplementedType? Then,
could be understood as
And
could be understood precisely as the type hints suggest.
As
NotImplementedTypeis available since Python 3.10 (and Python 3.9 reaches its end-of-life), type checkers would not need to implementNone-like handling forNotImplementedor supportLiteral[NotImplemented].Gradually adding
NotImplementedTypeto return type hints would hopefully work relatively smoothly. A few experiments with Mypy:For Pyright, the revealed types are a little different, but at least
D1() + D1()) + 1.0causes no wrong false positive (I am not familiar with Pyright and so do not know much aboutUnknown):Type checkers could, of course, implement a (transition) option that lets them continue to assume that all (in-place) binary special methods eventually return
NotImplemented.srittau commentedon Oct 23, 2025
I think the best solution would be the following annotations:
(This would rely on interpreting overloads in declaration order, though. As I understand it, that's not necessarily true anymore, though.)
Your suggestion of interpreting
def __add__(self, o: A1) -> float | NotImplementedType:like the explicit annotations above would necessitate further special handling in this case. It's also unclear what an annotation likedef __add__(self, o: A1 | int) -> float | str | NotImplementedType:would mean.tyralla commentedon Oct 23, 2025
would mean
But I see that repeating
NotImplementedTypein multiple overloads looks a bit silly:Yes, my suggestion to allow "implicit overloads" would require some special casing in type checkers, but it would reduce code repetition (
__add__,__radd__,__sub__...). And the binary special methods already require much special handling. Maybe a compromise could be to allow "implicit overloads" only for plain methods (without "normal overloads"). But I have no strong opinion here; I think your suggestion to require "explicit overloads" is equally fine.Maybe one could write a helpful decorator for this purpose?
srittau commentedon Oct 23, 2025
My main problem is not the special casing in type checkers, but the special casing in user's minds. Users need to know about it, and it's another special case for a not super common feature.
KotlinIsland commentedon Oct 24, 2025
some issues with my thoughts on this topic:
NotImplementedTypeas a return type astral-sh/ty#1150