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
Honor return type of __new__ even if not a subclass #15182
With mypy this is rejected and the type of B() is inferred incorrectly but pyright accepts this and gets the inference for B() correct:
$ mypy q.pyq.py:8: error: Incompatible return type for "__new__" (returns "A", but must return a subtype of "B") [misc]q.py:11: note: Revealed type is "q.B"Found 1 error in 1 file (checked 1 source file)
$ pyright q.py... ./q.py:11:13 - information: Type of "B()" is "A"0 errors, 0 warnings, 1 information Completed in 0.955sec
If the return type is Any, ignore that and keep the class type as
the return type
Otherwise respect __new__'s return type
Produce an error if the return type is not a subtype of the class.
The issue here is about mixing the last two points. Here mypy produces an error but then does not respect __new__'s return type. The return type is not respected since mypy infers that it is of type B unlike pyright which infers type A as specified by the hint. I don't mind mypy reporting an error here but in context -> A is the accurate type hint and mypy should respect that because anything else is just not correct. I can add a type ignore to silence the mypy error but the inference will still be wrong in all downstream/user code and is also inconsistent with pyright.
This is handled here where there is an explicit check to ignore the type given in __new__ if it is not a subtype of the class and also in a bunch of other cases:
I'm not sure where all of those conditions come from but I don't see how any of them can override the hint given in __new__ because __new__ can return anything (this is generally the reason for using __new__ rather than __init__).
Hi @oscarbenjamin, this seems to be a duplicate of #8330. I've opened #14471 since then but I'm kind of stuck on it. Hopefully if someone with the required knowledge could see what's wrong in the PR we could get it merged one day!
Yes, I saw that issue. I thought this was different but now that I've seen the code that causes this I can see that it causes both issues.
To be honest having looked at the code I don't really understand what any of it is doing. It doesn't bear any resemblance to Python's runtime semantics for type.__call__, __new__ and __init__. For example the code here chooses between taking a hint from __new__ or __init__ based on which comes earlier in the MRO:
# We take the type from whichever of __init__ and __new__ is first
# in the MRO, preferring __init__ if there is a tie.
That makes no sense to me:
__new__ and __init__ do not share method resolution so it does not matter which comes "first" in the MRO: __new__ is always called first and gets to decide whether __init__ is ever called at all.
The concrete type returned is determined solely by __new__ (leaving aside metaclasses overriding type.__call__) and importantly __new__ can return anything.
__init__ is irrelevant for determining the type of self: it is only ever called with an instance of the class in which it is defined and will not be called at all if __new__ returns something else.
I would expect the inference to work something like this. We have a class:
When we call A(*args, **kwargs) it will return Meta.__call__(A, *args, **kwargs) so we should determine what type that would return. By default Meta is type whose call method looks like:
classtype:
def__call__(A, *args, **kwargs):
# Use __new__ to create the objectifA.__new__isobject.__new__:
obj=object.__new__(A)
else:
obj=A.__new__(A, *args, **kwargs)
# Call __init__ with the created object (only if is an instance of A):ifisinstance(obj, A):
iftype(obj).__init__isnotobject.__init__:
obj.__init__(*args, **kwargs)
# Return the object obtained from __new__returnobj
The final piece to know is that object.__new__(A) returns an object of type A. The call to A.__new__(A, ...) needs further analysis but usually it will eventually come down to object.__new__(A) (via calls like B.__new__(A) along the way).
We see here that the type of the object returned is just the return type of A.__new__(). If A.__new__() returns an object that is not an instance of A then A.__init__() will never be called meaning any hints in A.__init__() are irrelevant. Even if A.__init__() is called it does not change the type of obj (unless it reassigns obj.__class__ but that is very obscure).
Like @oscarbenjamin noted, this fails in the contravariant case, i.e. when __new__ returns an instance of its supertype.
But when __new__ is annotated to return something else entirely, mypy will simply ignore it, even if annotated explicitly:
classOther:
def__new__(cls, arg: T, /) ->T: # error: "__new__" must return a class instance (got "T") [misc]returnargassert_type(Other(42), 'int') # error: Expression is of type "Other", not "int" [assert-type]
For most classes, the return type for the __new__ method is typically Self, but other types are also allowed. For example, the __new__ method may return an instance of a subclass or an instance of some completely unrelated class.
So this confirms that this is, in fact, a bug (and IMO a rather big one).
Reacted by Hash, Oscar Benjamin, Thomas Sturm, Siddhartha Gandhi, Mahadi Xion, studyingegret and Sveinung Gundersen
The relevant parts of the code have not changed since I linked them above. Within mypy __new__ is treated as an alternate version of __init__. There is no analysis of the sequence I described above (which matches the updated typing spec):
type.__call__
cls.__new__
obj.__init__
Having seen the code it is not surprising that other examples don't work as well (gh-14122):
@Viicos Ah good, that's better than nothing I guess. But it still doesn't constitute a full fix, because that error is a false negative.
I also wonder if it'll still "work" when __new__ is overloadaed.
Looking at the (updated) test cases, feels like it does?
Maybe in some cases but likely not others. As noted in the comments the code uses a "hack" to make this work for some cases. It begins by checking is_new which I believe is already incorrect because __new__ and __init__ are searched in a "combined MRO" as noted above.
I would expect that it still fails for other cases like this:
from __future__ importannotationsclassA:
def__new__(cls) ->A:
returnsuper().__new__(cls)
classB(A):
def__new__(cls) ->A:
returnsuper().__new__(cls)
classC(B):
def__init__(self): # possibly use self:C here.pass# C.__init__ is earlier in the "combined MRO" than B.__new__reveal_type(B()) # should be Areveal_type(C()) # should also be A
I think the overall logic in type_object_type and other places needs a rethink from first principles to be able to handle all cases. The presence of the is_new variable implies a confusion that __new__ and __init__ are somehow interchangeable so anything derived from it is likely incorrect for some cases.
This is also a blocker for correctly annotating the numpy.object_ constructor, as well as the constructors of (all) other scalar types in case of "multidimensional" input:
In the meantime, could Incompatible return type for "__new__" (returns "...", but must return a subtype of "...") and "__new__" must return a class instance (got <some union>) be made into a separate error code than misc ? I can't really ignore them without ignoring the entirety of misc atm.
Activity
oscarbenjamin commentedon May 4, 2023
This is handled here where there is an explicit check to ignore the type given in
__new__if it is not a subtype of the class and also in a bunch of other cases:mypy/mypy/typeops.py
Lines 184 to 196 in 13f35ad
I'm not sure where all of those conditions come from but I don't see how any of them can override the hint given in
__new__because__new__can return anything (this is generally the reason for using__new__rather than__init__).Viicos commentedon May 4, 2023
Hi @oscarbenjamin, this seems to be a duplicate of #8330. I've opened #14471 since then but I'm kind of stuck on it. Hopefully if someone with the required knowledge could see what's wrong in the PR we could get it merged one day!
oscarbenjamin commentedon May 4, 2023
Yes, I saw that issue. I thought this was different but now that I've seen the code that causes this I can see that it causes both issues.
To be honest having looked at the code I don't really understand what any of it is doing. It doesn't bear any resemblance to Python's runtime semantics for
type.__call__,__new__and__init__. For example the code here chooses between taking a hint from__new__or__init__based on which comes earlier in the MRO:mypy/mypy/checkmember.py
Lines 1198 to 1199 in 13f35ad
That makes no sense to me:
__new__and__init__do not share method resolution so it does not matter which comes "first" in the MRO:__new__is always called first and gets to decide whether__init__is ever called at all.__new__(leaving aside metaclasses overridingtype.__call__) and importantly__new__can return anything.__init__is irrelevant for determining the type of self: it is only ever called with an instance of the class in which it is defined and will not be called at all if__new__returns something else.I would expect the inference to work something like this. We have a class:
When we call
A(*args, **kwargs)it will returnMeta.__call__(A, *args, **kwargs)so we should determine what type that would return. By defaultMetaistypewhose call method looks like:The final piece to know is that
object.__new__(A)returns an object of typeA. The call toA.__new__(A, ...)needs further analysis but usually it will eventually come down toobject.__new__(A)(via calls likeB.__new__(A)along the way).We see here that the type of the object returned is just the return type of
A.__new__(). IfA.__new__()returns an object that is not an instance ofAthenA.__init__()will never be called meaning any hints inA.__init__()are irrelevant. Even ifA.__init__()is called it does not change the type ofobj(unless it reassignsobj.__class__but that is very obscure).NeilGirdhar commentedon Dec 19, 2023
Respecting the metaclass'
__call__return type is required to type check nnx, which makes extensive use of this feature here.Viicos commentedon Dec 20, 2023
As a workaround, I'm using the
__new__method instead (see #16020 (comment), where I'm hitting the same use case as you)jorenham commentedon Mar 25, 2024
This currently makes it impossible to correctly type
builtins.reversed. See python/typeshed#11645 and python/typeshed#11646jorenham commentedon Mar 25, 2024
Like @oscarbenjamin noted, this fails in the contravariant case, i.e. when
__new__returns an instance of its supertype.But when
__new__is annotated to return something else entirely, mypy will simply ignore it, even if annotated explicitly:For details, see https://mypy-play.net/?mypy=latest&python=3.12&flags=strict&gist=336602d390b5e6566e9fca93d7fa48a6
Viicos commentedon Mar 25, 2024
The PR fixing this can be found here: #16020
jorenham commentedon Jul 5, 2024
@Viicos #16020 does not fix this issue. In the PR description it explicit states:
jorenham commentedon Jul 5, 2024
The official typing specs now include a section on the
__new__method:So this confirms that this is, in fact, a bug (and IMO a rather big one).
oscarbenjamin commentedon Jul 5, 2024
The relevant parts of the code have not changed since I linked them above. Within mypy
__new__is treated as an alternate version of__init__. There is no analysis of the sequence I described above (which matches the updated typing spec):type.__call__cls.__new__obj.__init__Having seen the code it is not surprising that other examples don't work as well (gh-14122):
Viicos commentedon Jul 6, 2024
Looking at the (updated) test cases, feels like it does? In particular:
mypy/test-data/unit/check-classes.test
Lines 6925 to 6931 in cdccffa
While a type ignore comment is needed,
intis not a subclass ofAand it type checks as expected.jorenham commentedon Jul 6, 2024
@Viicos Ah good, that's better than nothing I guess. But it still doesn't constitute a full fix, because that error is a false negative.
I also wonder if it'll still "work" when
__new__is overloadaed.oscarbenjamin commentedon Jul 6, 2024
Maybe in some cases but likely not others. As noted in the comments the code uses a "hack" to make this work for some cases. It begins by checking
is_newwhich I believe is already incorrect because__new__and__init__are searched in a "combined MRO" as noted above.I would expect that it still fails for other cases like this:
I think the overall logic in
type_object_typeand other places needs a rethink from first principles to be able to handle all cases. The presence of theis_newvariable implies a confusion that__new__and__init__are somehow interchangeable so anything derived from it is likely incorrect for some cases.jorenham commentedon Feb 28, 2025
This is also a blocker for correctly annotating the
numpy.object_constructor, as well as the constructors of (all) other scalar types in case of "multidimensional" input:See #18343 (comment) for details.
Avasam commentedon May 25, 2025
In the meantime, could
Incompatible return type for "__new__" (returns "...", but must return a subtype of "...")and"__new__" must return a class instance (got <some union>)be made into a separate error code thanmisc? I can't really ignore them without ignoring the entirety ofmiscatm.__new__return types are not respected (reported as error and overwritten by the current class) unless the return type is a subclass of the current class #19624 as a duplicate of this issueRespect explicit return type of __new__() (python#21441)