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
Self and self are not bound until after class definition, becoming Any/Never when passed through a decorator #16554
There is occasional use for a method that exhibits different behaviours when accessed via an instance vs the class. SQLAlchemy has one such example named hybrid_method (code). I was experimenting with a generic decorator for this pattern and found what appear to be two bugs in Mypy 1.7.0. The code works at runtime and passes type validation in Pyright 1.1.337 (barring a no-redef error).
First problem, given the following code pattern:
classHost:
# T = TypeVar, P = ParamSpec, R = TypeVar@Decorator# (Callable[Concatenate[T, P], R]) -> Decorator[Generic[T, P, R]]defmethod(self) ->Self:
returnselfreveal_type(method) # T=Never, R=Any
Type T is self and R is Self, and both should be the same. The decorator has to be Generic[P, R] to return Callable[P, R] in __get__, but R then turns into Any. If the decorator is Generic[T, P, R], then R becomes Never and Host().method() errors on the self parameter: expected Never, got Host. reveal_type shows that self's type is lost within the class definition itself. Pyright processes it correctly.
Second problem, when the decorator implements the @prop.setter pattern to replace itself with a new object with different generic binds. Mypy raises a no-redef error, which can be type-ignored, but the revealed type continues to show the old generic binds. This problem goes away when the second method is given a different name.
To Reproduce
Here's sample code with a functioning decorator and three ways to use it. This code works at runtime and passes Pyright (barring one redef), but each of these versions has different errors or loss of type information in Mypy.
I think Mypy's handling of self isn't completely wrong, even though it breaks typing via the decorator but works for built-ins like @property and @classmethod. The type of the self/cls parameter is only known at call time. The type assigned at definition time (host class's type or a subtype) is only the usual type for the usual descriptor-based access (cls.method or obj.method). Which means the problem is in the decorator's typing for its descriptor:
Okay, checked this up. Specifying a type on self in Protocol.__call__ makes it not callable, so dead-end there.
Pyright also correctly processes Self when the decorated method is accessed via a subclass. I tried the pre-Self method of a bound TypeVar but that doesn't work in both Pyright and Mypy:
PEP 673 – Self Type makes no mention of how Self should be processed in a generic decorator except for @classmethod (which is anyway special-cased in type checkers), so this looks like a gap in the spec.
The difference in how Pyright and Mypy bind Self comes out when calling an unbound method:
So: Pyright is binding Self from the class or instance, which is usually right, but wrong when calling an unbound method, whereas Mypy is more accurately binding it from the self parameter, but that is lost in the decorator's __get__ signature. If only there was a way to add it back with something like MethodType[T, Callable[P, R]] (or with Protocol).
Pyright is binding Self from the class or instance ... [which is] wrong when calling an unbound method
I think this result is still correct from a type perspective. In your example above, the runtime produces a value which is a Child instance, and this is a valid subtype of Parent, which is the type evaluated by pyright. In other words, this isn't a "wrong" type evaluation. It's perhaps not as narrow as you'd like, but that can't be helped if binding is done when it should be.
I think binding and partial specialization must be done when evaluating the attribute access expression (Parent.method), not as part of the call expression (Parent.method(s1)). This becomes clear when you consider generic classes. Consider if Parent were generic and accepted a single type parameter, as in the modified example below:
fromtypingimportGeneric, Self, TypeVarT=TypeVar("T")
classParent(Generic[T]):
defmethod(self) ->Self:
returnselfclassChild(Parent[int]):
passs1=Child()
s2=Parent[str].method(s1) # Pyright: Type violation (correct), mypy: no errorreveal_type(s2)
Here, mypy's approach results in a clear false negative.
Binding rules are admittedly underspecified in the Python typing spec. This would be a good topic for the newly-formed typing council to clarify so we can get consistent behavior between type checkers.
I think binding and partial specialization must be done when evaluating the attribute access expression
I agree. This has the nice side effect that the generic decorator is simpler because it doesn't have to bind on type.
In my first code sample above, I made the mistake of defining self type as invariant because that's the default for TypeVar, but self has to be covariant. I had no idea what these terms meant last month, and I imagine it's going to cause grief to many others writing type annotations for the first time.
Activity
jace commentedon Nov 25, 2023
I think Mypy's handling of self isn't completely wrong, even though it breaks typing via the decorator but works for built-ins like
@propertyand@classmethod. The type of the self/cls parameter is only known at call time. The type assigned at definition time (host class's type or a subtype) is only the usual type for the usual descriptor-based access (cls.methodorobj.method). Which means the problem is in the decorator's typing for its descriptor:There is no indication that the Callable is typed T, so the
Selftype in R can't be linked to T. Maybe it can be done using a Protocol?jace commentedon Nov 25, 2023
Okay, checked this up. Specifying a type on
selfinProtocol.__call__makes it not callable, so dead-end there.Pyright also correctly processes
Selfwhen the decorated method is accessed via a subclass. I tried the pre-Self method of a bound TypeVar but that doesn't work in both Pyright and Mypy:PEP 673 – Self Type makes no mention of how
Selfshould be processed in a generic decorator except for@classmethod(which is anyway special-cased in type checkers), so this looks like a gap in the spec.The difference in how Pyright and Mypy bind
Selfcomes out when calling an unbound method:So: Pyright is binding
Selffrom the class or instance, which is usually right, but wrong when calling an unbound method, whereas Mypy is more accurately binding it from theselfparameter, but that is lost in the decorator's__get__signature. If only there was a way to add it back with something likeMethodType[T, Callable[P, R]](or with Protocol).Related: #16558, cc: @erictraut and @AlexWaygood
jace commentedon Nov 25, 2023
Test code: https://mypy-play.net/?mypy=latest&python=3.11&gist=e8de541c111bccf640907937f8794a1b
erictraut commentedon Nov 25, 2023
I think this result is still correct from a type perspective. In your example above, the runtime produces a value which is a
Childinstance, and this is a valid subtype ofParent, which is the type evaluated by pyright. In other words, this isn't a "wrong" type evaluation. It's perhaps not as narrow as you'd like, but that can't be helped if binding is done when it should be.I think binding and partial specialization must be done when evaluating the attribute access expression (
Parent.method), not as part of the call expression (Parent.method(s1)). This becomes clear when you consider generic classes. Consider ifParentwere generic and accepted a single type parameter, as in the modified example below:Here, mypy's approach results in a clear false negative.
Binding rules are admittedly underspecified in the Python typing spec. This would be a good topic for the newly-formed typing council to clarify so we can get consistent behavior between type checkers.
jace commentedon Nov 25, 2023
I agree. This has the nice side effect that the generic decorator is simpler because it doesn't have to bind on type.
In my first code sample above, I made the mistake of defining self type as invariant because that's the default for TypeVar, but self has to be covariant. I had no idea what these terms meant last month, and I imagine it's going to cause grief to many others writing type annotations for the first time.