Repository navigation
Closed as not planned
Closed as not planned
Copy link
Labels
as designedNot a bug, working as intendedNot a bug, working as intendedbugSomething isn't workingSomething isn't working
Description
This example illustrates the issue, and how solving it makes it possible to completely annotate assert type(x) is int:
from typing import Protocol, override
class Just[T](Protocol): # ~T@Just is invariant
@property # type: ignore[override]
@override
def __class__(self, /) -> type[T]: ... # pyright: ignore[reportIncompatibleMethodOverride]
@__class__.setter
def __class__(self, t: type[T], /) -> None: ...
###
def just_int(x: Just[int], /) -> None:
assert type(x) is int
just_int(42) # true negative
just_int(False) # false negative (works with mypy)
just_int("") # true positive, wrong error message: Type "int | str" is not assignable to type "int"
###
def just_float(x: Just[float], /) -> None:
assert type(x) is float
just_float(3.14) # true negative
just_float(42) # false negative (also with mypy)
just_float(False) # false negative (also with mypy)
just_float("") # true positive, wrong error message: Type "float | str" is not assignable to type "float"The error messages make me suspect that __class__: T is interpreted as T | Self.
This partially works in mypy:
https://mypy-play.net/?mypy=latest&python=3.12&gist=c50b6d2092cf463c2b97d9267fa21896
Reacted by KotlinIsland and Denis Lusson
Metadata
Metadata
Assignees
Labels
as designedNot a bug, working as intendedNot a bug, working as intendedbugSomething isn't workingSomething isn't working
Activity
erictraut commentedon Nov 7, 2024
Pyright is working as intended here, so I don't consider this a bug.
The type
Just[int]is a specialization of theJustclass, so we can effectively rewrite it as a non-generic class that looks like this:Now, let's consider the relevant parts of the
boolclass that are required to satisfy this protocol definition. Note that the definition of__class__is inherited byboolfromobject, but we'll simplify it here.Does
SimplifiedBoolsatisfy theJusIntprotocol? Yes, it does;type[bool]is a subtype oftype[int], andtype[object]is a supertype oftype[int]. Note that we used a covariant check for the return type and a contravariant check for the parameter type.Mypy seems to think that
SimplifiedBoolis not assignable toJustInt. I can't explain why. This looks to me like a bug in mypy.Let's see if this has to do with
__class__. After all,JustIntis overridingobject.__class__in a way that violates Liskov, so maybe that's confusing mypy.Once again, mypy is generating an error here for reasons I can't explain. Maybe it's a bug in mypy's handling of properties, which requires special casing? Let's switch to a non-property getter and setter to see if that results in different behavior.
Now mypy agrees that the concrete class meets the requirements of the protocol. So this appears to be a bug in mypy's handling of properties within protocols.
jorenham commentedon Nov 10, 2024
Given the current typeshed annotations of
object.__class__, then that is indeed the case. But they're not quite right:The return type of the
__class__getter should be the very same as the input type of its setter, so it should be invariant.For me it "clicked" when I started looking at
__class__as a regular attribute, albeit one that cannot be deleted, which is why the@propertyis needed here.So the
Selfcan be seen as a hidden type-parameter that's invariant w.r.t.object, and that's bound to the most specificobject(sub)type.A nice consequence of this is that it's no longer allowed to do unsafe things such as
So not only will this prevent Liskov from getting violent, it'll also prevent the
Selffrom changing.And as far as I'm concerned, that sounds like a pretty good deal.
I guess I should probably raise this with typeshed.
jorenham commentedon Nov 10, 2024
There's also something that's
__class__-specific, here. Because when I do a similar thing with some other property thatobjectdoesn't have, then pyright behaves in the same way as mypy:pyright play
jorenham commentedon Nov 11, 2024
The (true positive) error message for
just_int("")also suggests that this isn't only a covariance/invariance thing, but that there's something else going on:So somewhere along the way, the
-> type[int]seems to have been changed into-> type[int | str].edit
pyright/packages/pyright-internal/src/analyzer/binder.ts
Lines 531 to 532 in 294b7af
I'm not very familiar with typescript, but perhaps this is what's causing this issue?
jorenham commentedon Nov 12, 2024
This also seems relevant
pyright/packages/pyright-internal/src/analyzer/symbol.ts
Lines 40 to 42 in 294b7af
jorenham commentedon Nov 12, 2024
Eric Traut (@erictraut) would you mind taking another look at this?
erictraut commentedon Nov 12, 2024
As I said above, pyright is working as intended here. Please refer to my analysis above.
jorenham commentedon Nov 12, 2024
And did you read my response to it Eric Traut (@erictraut) ? Because there I explain in detail that this isn't about the Liskov substitution principle. This is an issue about a special-casing of
__class__that's causing trouble.I also posted the the exact code that I think is responsible for this.
erictraut commentedon Nov 12, 2024
Pyright doesn't special-case the class-scoped
__class__symbol. It relies on the typeshed definition. Pyright did special-case this symbol (and a few others) many years ago prior to typeshed adding these definitions. That explains the (stale) comment next to theIgnoredForProtocolMatchflag that you cited above.The code in the binder that you cited adds an implicit
__class__symbol to a method scope. It handles the following use case. This is unrelated to protocol matching or variance calculation because function-local symbols are not considered for protocol matching.In your analysis above, you said that you think there's something specific to
__class__. I'm not seeing that. I think you made a mistake in your analysis because you've definedBoolThingas follows:This doesn't reflect the actual definition of
bool.__class__, which uses atype[object]in its setter's signature. If you change the setter forBoolThing.thingto usetype[object]rather thantype[bool], you'll see behavior that is consistent with__class__.If you think that the typeshed definition of the
__class__symbol is incorrect, please report the issue to the typeshed maintainers.In summary, I think my analysis above is correct, and pyright's behavior is both consistent and in conformance with the typing spec.
jorenham commentedon Nov 12, 2024
Ah of course; my bad.
python/typeshed#12997
You see, thereason why I was thinking it was special cased, is the following error message from my first example:
And I now realize that this can also be reproduced by adding a
StrThing(with_: objectas setter param):I still don't really get where that
int |part of the error message comes from, but I'm sure you can see how it could be confusing in such cases.Anyway, thanks for the help :)
jorenham commentedon Nov 18, 2024
This is now fixed in typeshed Eric Traut (@erictraut): python/typeshed#13021