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
Respecting return type of __new__ does not extend to derived classes #8330
I think I'm hitting this issue when trying to type an overloaded __new__ in a base class
My attempt overloading __new__ to match up with the untyped source:
from __future__ importannotationsfromtypingimportAny, Dict, List, Type, TypeVar, overloadfromtyping_extensionsimportLiteralT=TypeVar("T", bound="MiniBaseSerializer")
classMiniBaseSerializer:
@overloaddef__new__(cls, *, many: Literal[True]) ->MiniListSerializer:
...
@overloaddef__new__(cls: Type[T], *, many: Literal[False] = ...) ->T:
...
def__new__(cls: Type[T], many: bool= ...) ->Any:
...
classMiniListSerializer(MiniBaseSerializer):
@propertydefvalidated_data(self) ->List[Dict[str, object]]:
...
classMiniSerializer(MiniBaseSerializer):
@propertydefvalidated_data(self) ->Dict[str, object]:
...
classExampleSerializer(MiniSerializer):
# define some fields
...
example_many=ExampleSerializer(many=True)
reveal_type(example_many)
# pyright: Type of "example_many" is "MiniListSerializer"# mypy: Revealed type is 'test_foo.ExampleSerializer'example_single=ExampleSerializer()
reveal_type(example_single)
# pyright: Type of "example_many" is "MiniListSerializer"# mypy: Revealed type is 'test_foo.ExampleSerializer*'
It works in pyright, but mypy forgets about the __new__ in the base class
I then tried overloading __init__ and adding a generic, which sort of works, but would require any users to provide a generic type param and it is also kind of lying about what is happening since the actual source code is returning a different type by overloading __new__
from __future__ importannotationsfromtypingimportDict, Generic, List, TypeVar, overloadfromtyping_extensionsimportLiteralT=TypeVar("T")
classMiniBaseSerializer(Generic[T]):
@overloaddef__init__(self: MiniBaseSerializer[List[Dict[str, object]]], many: Literal[True]) ->None:
...
@overloaddef__init__(self: MiniBaseSerializer[Dict[str, object]], many: Literal[False] = ...) ->None:
...
def__init__(self, many: bool=False) ->None:
...
@propertydefvalidated_data(self: MiniBaseSerializer[T]) ->T:
...
classMiniSerializer(Generic[T], MiniBaseSerializer[T]):
...
classExampleSerializer(MiniSerializer[object]):
...
example_many=MiniBaseSerializer(many=True)
reveal_type(example_many)
# pyright - Type of "example_many" is "MiniBaseSerializer[List[Dict[str, object]]]"# mypy - Revealed type is 'test_bar.MiniBaseSerializer[builtins.list[builtins.dict[builtins.str, builtins.object]]]'example_single=MiniBaseSerializer()
reveal_type(example_single)
# pyright - Type of "example_single" is "MiniBaseSerializer[Dict[str, object]]"# mypy - Revealed type is 'test_bar.MiniBaseSerializer[builtins.dict[builtins.str, builtins.object]]'
In search the issues, I also found #9482 which seemed related
Since then, subprocess.Popen in typeshed no longer defines __new__ which solves the concrete example in #7597 AFAICT.
Also with PEP 673 Self type freshly merged in #11871, it should be easier to declare __new__ methods that correctly apply to subclasses as def __new__(...) -> Self, removing another reason for this restriction.
Returning non-subclasses from __new__() is a hack for sure, but as explained in #8330 (comment) and #14426 there are real-world libraries like Django REST Framework which are using this hack. And this restriction prevents accurately type hinting this library.
I've tried reverting this specific check (Viicos@016abc3), but according to the mypy_primer output, this seems to break a lot of things (my fix is probably wrong)
Returning non-subclasses from \_\_new\_\_() is a hack for sure, but as explained in #8330 (comment) and #14426 there are real-world libraries like Django REST Framework which are using this hack. And this restriction prevents accurately type hinting this library.
Should it even matter if something is a hack or matter if it's been proven to be useful? Wouldn't that be a discussion for the language and/or typing standards (or possibly linter) more than for the implementation of the reference type checker? It seems to be true that arbitrary return values from __new__ are legal python, and intentionally so.
Should it even matter if something is a hack or matter if it's been proven to be useful? Wouldn't that be a discussion for the language and/or typing standards (or possibly linter) more than for the implementation of the reference type checker? It seems to be true that arbitrary return values from new are legal python, and intentionally so.
This is the same idea as stated here: #1020 (comment), and I do agree. I haven't got time to get back to the PR, but I'll hope it will be merged when ready in the future
Please make sure that if the __new__ return is untyped (returns Any) then it uses the class type implicitly. Not sure if that is already covered, but I suspect not.
I'm sure I'm not as familiar with realities, but specs are desirable. From PEP484:
Any function without annotations should be treated as having the most general type possible, or ignored, by any type checker. "
How do you get from that to restricting an explicit user-provided __new__ function to a particular type by default?
Neither mypy nor pyright are perfect, but an example:
Pyright accepts the above, with (case 1) or without(case 2) the ->Any , but mypy wrongly complains about shifty howling. And really, either seems to be legal python in spec and in implementation. As I understand, intgr's proposal would reject them (edit:) and would require legal code to have explicit type annotations to pass mypy Adding a typed parameter to __new__ changes nothing in both cases.
The rest may be beyond the PR, but strongly relate:
shifty.bleat()
Case 3: Still, fine for Pyright unless you remove the Any (Case 4).
Then Pyright sees the object.new(Wolf) and knows shifty cannot bleat and complains. PEP484:
For a checked function, the default annotation for arguments and for the return type is Any.
Intgr also implies that -> Any and no annotation should be treated the same, so it seems Pyright misses the spec, at least in the case where there's a typed parameter guaranteeing that it's a "checked function."
What about type narrowing with no type hints at all. With no return type hints, Pyright type narrows (deduces) Sheep.__new__() to Wolf.
Is narrowing part of "checking"? Is it legal to do type narrowing inside an un-"checked" function? If it's possible to determine with certainty that a runtime type error will definitely occur, should a checker ever be implicitly required to ignore that to be standard compliant? The first quote says "most general type OR ignored." A general type can be narrowed by other information.
On the other hand, when a value has type Any (default), the type checker will allow all operations on it,
Sure but can the Any return type be narrowed before/when returning?
Another example:
classmouse():
passjerry=mouse()
mouse.howl()
(Case 5) Pyright and mypy both complain this time. But mouse() is now guaranteed to be a mouse, and
and we have no wrapper around object.new('T') in which to ignore checking (or maybe narrowing).
(Case 6) Supershifty certainly cannot both bleat and howl. No matter what, this a type error. Mypy as always just ignores our new and only complains about howl(). It would be happy if we only tried to bleat.
Pyright again behaves differently with no return type (case 6) than with -> Any. With no return hint, (edit: it seems to assume a union type) and complains about both bleating and howling, referencing the respective problematic type for each. With -> Any (Case 7) it relents and gives no error.
Should the first access be accepted but treated as narrowing, only creating an error on the conflicting one? Would it be different if the conditional used a random bool, or a flip flop instead of an input? Inputs can be constrained. How smart must the checker be and what assumptions can/should it make?
I find it hard to read the PEP's and have any expectations, especially correct ones, about what mypy may or may not do. Does the PEP need to be improved?
@DougLeonard you are raising a lot of interesting questions here, and regarding the return object.__new__(Wolf) from the Sheep class, I'm wondering if mypy can infer that the return type is Wolf, as pyright is currently doing. We can't really know right now, as Wolf is not a subclass of Sheep hence why mypy ignores it (and this is what the PR is trying to fix).
Regarding the last example, maybe the type checker could infer a union of both types? My knowledge is limited here, but I can say that narrowing is part of type checking, and I'm still seeing some PRs implementing new kinds of type narrowing.
@Viicos, Right but pyright is inferring Wolf even when there are no type annotations at all. Should it? You said narrowing is part of checking. I'm not sure if it's right for me to call inferring/deducing Wolf as a kind of narrowing on the implicit -> Any, but the spec says we should not do type checking on un-annotated functions, and it seems they should just be assumed to return Any. Personally, I'd rather see that it is always at least allowable to apply deductions that are guaranteed to be true, and it seems always a desirable feature. If the spec presently says that such deductions/narrowing are actually illegal to do on un-annotated functions, then maybe the spec should be changed. But I'm not sure I'd say (from a naive reading) that the wording must mean that no checking means no deducing. Anyway, presently mypy is of course deducing anyway, but is deducing wrongly.
For the last example pyright seems to infer a union (when ->Any isn't explicit), as it does complain about each of the types respectively. Yes, I think that's pretty good.
Activity
ilevkivskyi commentedon Jan 25, 2020
This is actually intentional, because doing the opposite (as it was initially implemented) caused even more false positives.
I am not sure what to do here, cc @msullivan
ikonst commentedon Jan 26, 2020
Curious, point to discussion / false positives?
sbdchd commentedon Jul 9, 2021
I think I'm hitting this issue when trying to type an overloaded
__new__in a base classMy attempt overloading
__new__to match up with the untyped source:It works in
pyright, butmypyforgets about the__new__in the base classI then tried overloading
__init__and adding a generic, which sort of works, but would require any users to provide a generic type param and it is also kind of lying about what is happening since the actual source code is returning a different type by overloading__new__In search the issues, I also found #9482 which seemed related
Edit: Additionally, in the linked PR, there is:
mypy/test-data/unit/check-classes.test
Lines 6276 to 6288 in 30efa34
which has the subclass maintaining the
__new__defined on the superclassViicos commentedon Jan 12, 2023
As stated in typeddjango/djangorestframework-stubs#315,
django-rest-frameworkis a good example that would benefit from the requested behavior.I'm curious to hear about the false positives that this caused
intgr commentedon Jan 12, 2023
Finally found the PR and issue that introduced this restriction:
Since then,
subprocess.Popenin typeshed no longer defines__new__which solves the concrete example in #7597 AFAICT.Also with PEP 673
Selftype freshly merged in #11871, it should be easier to declare__new__methods that correctly apply to subclasses asdef __new__(...) -> Self, removing another reason for this restriction.Returning non-subclasses from
__new__()is a hack for sure, but as explained in #8330 (comment) and #14426 there are real-world libraries like Django REST Framework which are using this hack. And this restriction prevents accurately type hinting this library.ikonst commentedon Jan 17, 2023
Thanks for the thorough code archeology, @intgr!
I suppose we can try adjusting it once more and seeing what breaks in mypy-primer etc.
Viicos commentedon Jan 18, 2023
I've tried reverting this specific check (Viicos@016abc3), but according to the
mypy_primeroutput, this seems to break a lot of things (my fix is probably wrong)intgr commentedon Jan 18, 2023
Maybe you need to replace it with logic that retains the old behavior if
__new__return type wasAny(e.g. untyped).Where can I see the mypy_primer results?
Viicos commentedon Jan 18, 2023
I ran mypy_primer locally, but I'll make the PR so that we can have the output
DougLeonard commentedon Feb 17, 2023
Should it even matter if something is a hack or matter if it's been proven to be useful? Wouldn't that be a discussion for the language and/or typing standards (or possibly linter) more than for the implementation of the reference type checker? It seems to be true that arbitrary return values from __new__ are legal python, and intentionally so.
Viicos commentedon Feb 17, 2023
This is the same idea as stated here: #1020 (comment), and I do agree. I haven't got time to get back to the PR, but I'll hope it will be merged when ready in the future
DougLeonard commentedon Feb 25, 2023
quoting intgr from PR thread #14471:
I'm sure I'm not as familiar with realities, but specs are desirable. From PEP484:
How do you get from that to restricting an explicit user-provided __new__ function to a particular type by default?
Neither mypy nor pyright are perfect, but an example:
Pyright accepts the above, with (case 1) or without(case 2) the ->Any , but mypy wrongly complains about shifty howling.
And really, either seems to be legal python in spec and in implementation. As I understand, intgr's proposal would reject them (edit:) and would require legal code to have explicit type annotations to pass mypy Adding a typed parameter to __new__ changes nothing in both cases.
The rest may be beyond the PR, but strongly relate:
Case 3: Still, fine for Pyright unless you remove the Any (Case 4).
Then Pyright sees the object.new(Wolf) and knows shifty cannot bleat and complains. PEP484:
Intgr also implies that -> Any and no annotation should be treated the same, so it seems Pyright misses the spec, at least in the case where there's a typed parameter guaranteeing that it's a "checked function."
What about type narrowing with no type hints at all. With no return type hints, Pyright type narrows (deduces) Sheep.__new__() to Wolf.
Is narrowing part of "checking"? Is it legal to do type narrowing inside an un-"checked" function? If it's possible to determine with certainty that a runtime type error will definitely occur, should a checker ever be implicitly required to ignore that to be standard compliant? The first quote says "most general type OR ignored." A general type can be narrowed by other information.
Sure but can the Any return type be narrowed before/when returning?
Another example:
(Case 5) Pyright and mypy both complain this time. But mouse() is now guaranteed to be a mouse, and
and we have no wrapper around object.new('T') in which to ignore checking (or maybe narrowing).
How about something more challenging?
(Case 6) Supershifty certainly cannot both bleat and howl. No matter what, this a type error. Mypy as always just ignores our new and only complains about howl(). It would be happy if we only tried to bleat.
Pyright again behaves differently with no return type (case 6) than with -> Any. With no return hint, (edit: it seems to assume a union type) and complains about both bleating and howling, referencing the respective problematic type for each. With -> Any (Case 7) it relents and gives no error.
Should the first access be accepted but treated as narrowing, only creating an error on the conflicting one? Would it be different if the conditional used a random bool, or a flip flop instead of an input? Inputs can be constrained. How smart must the checker be and what assumptions can/should it make?
I find it hard to read the PEP's and have any expectations, especially correct ones, about what mypy may or may not do. Does the PEP need to be improved?
Viicos commentedon Mar 5, 2023
@DougLeonard you are raising a lot of interesting questions here, and regarding the
return object.__new__(Wolf)from theSheepclass, I'm wondering if mypy can infer that the return type isWolf, as pyright is currently doing. We can't really know right now, asWolfis not a subclass ofSheephence why mypy ignores it (and this is what the PR is trying to fix).Regarding the last example, maybe the type checker could infer a union of both types? My knowledge is limited here, but I can say that narrowing is part of type checking, and I'm still seeing some PRs implementing new kinds of type narrowing.
DougLeonard commentedon Mar 8, 2023
@Viicos, Right but pyright is inferring Wolf even when there are no type annotations at all. Should it? You said narrowing is part of checking. I'm not sure if it's right for me to call inferring/deducing Wolf as a kind of narrowing on the implicit -> Any, but the spec says we should not do type checking on un-annotated functions, and it seems they should just be assumed to return Any. Personally, I'd rather see that it is always at least allowable to apply deductions that are guaranteed to be true, and it seems always a desirable feature. If the spec presently says that such deductions/narrowing are actually illegal to do on un-annotated functions, then maybe the spec should be changed. But I'm not sure I'd say (from a naive reading) that the wording must mean that no checking means no deducing. Anyway, presently mypy is of course deducing anyway, but is deducing wrongly.
For the last example pyright seems to infer a union (when ->Any isn't explicit), as it does complain about each of the types respectively. Yes, I think that's pretty good.
erictraut commentedon Mar 8, 2023
For details about areas where pyright intentionally differs from mypy, you may find this documentation interesting.
DougLeonard commentedon Mar 8, 2023
Thanks. I've seen it. And of course pyright can do whatever pyright wants to do. I believe mypy is meant to follow spec, but then neither really do.
ikonst commentedon Mar 9, 2023
@erictraut, a very exhaustive list. Can recognize some mypy pet peeves right there. BTW,
should no longer be the case since #13838 😃
Respect explicit return type of __new__() (python#21441)