Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cached_property self type #9464

Closed
cole-dda opened this issue Nov 14, 2024 · 2 comments
Closed

cached_property self type #9464

cole-dda opened this issue Nov 14, 2024 · 2 comments
Labels
as designed Not a bug, working as intended bug Something isn't working

Comments

@cole-dda
Copy link

Environment data

  • Pylance version: v2024.11.2
  • OS and version: macos 12.7.6
  • Python version (& distribution if applicable, e.g. Anaconda): python3.12

Code Snippet

class CachedBug:

    @cached_property
    def x(self)->int:
        return 0
    
    @cached_property
    def y(self)->str:
        return ''
    
    @cached_property
    def z(self)->Self:
        return self.__class__()
    
    @cached_property
    def a(self)->'CachedBug':
        return CachedBug()

def usage():
    bug = CachedBug()
    #x is int,ok
    x = bug.x
    #y is str,ok
    y = bug.y
    
    #z is Any,not ok
    z = bug.z

    #a is CachedBug,ok
    a = bug.a

Repro Steps

  1. use vscode open python file

Expected behavior

pylance can know "z" is CachedBug type

Actual behavior

"z" is Any type

Logs

XXX
@erictraut
Copy link
Collaborator

Could someone from the pylance team please transfer this to the pyright project? Thanks!

@debonte debonte transferred this issue from microsoft/pylance-release Nov 14, 2024
@erictraut erictraut added the bug Something isn't working label Nov 14, 2024
@erictraut
Copy link
Collaborator

Let's take a closer look at what's going on here. I'm going to simplify your example and copy a simplified version of cached_property from the typeshed stub functools.pyi. Of particular note is the Any used in the __init__ method.

from typing import Any, Callable, Self

class cached_property[T]:
    def __init__(self, func: Callable[[Any], T]) -> None: ...
    def __get__(self, instance: object, owner: type[Any] | None = None) -> T: ...

class CachedBug:
    @cached_property
    def z(self) -> Self:
        return self.__class__()

bug = CachedBug()
reveal_type(bug.z)  # Type is "Any"

The Self symbol represents a type variable implicitly defined and scoped to the class that contains it and with an upper bound of that class. We can manually replace Self with an explicit type variable with the same properties like this:

class CachedBug:
    @cached_property
    def z[S: CachedBug](self: S) -> S:
        return self.__class__()

This replacement makes it clearer what's happening. When the function z is passed to the decorator cached_property, pyright's constraint solver is presented with the following constraints:

S <= Any
T >= S

The solution to this set of constraints is S = Any and T = Any. Applying these solutions, the decorator call yields a return type of cached_property[Any]. And the specialized type of the cached_property[Any].getmethod has a return type ofAny`.

Interestingly, mypy (another Python type checker) appears to treat the type variable S: CachedBug and Self differently. It yields the results you are expecting in the case of Self. However, mypy has a number of known bugs and inconsistencies in its handling of Self that pyright does not. Refer to these conformance test results (and search for the "generics_self_advanced" test in particular). So I suspect this inconsistency is actually a bug in mypy rather than evidence of a bug in pyright.

from typing import Any, Callable, Self

class cached_property[T]:
    def __init__(self, func: Callable[[Any], T]) -> None: ...
    def __get__(self, instance: object, owner: type[Any] | None = None) -> T: ...

class CachedBug:
    @cached_property
    def z1[S: CachedBug](self: S) -> S:
        return self.__class__()

    @cached_property
    def z2[S: CachedBug](self) -> Self:
        return self.__class__()

bug = CachedBug()
reveal_type(bug.z1)  # Mypy says: "Any", pyright says: "Any"
reveal_type(bug.z2)  # Mypy says: "CachedBug", pyright says: "Any"

In summary, I think pyright is doing the right thing here — a behavior that is consistent with the Python typing spec. I understand that the resulting behavior is less than satisfying in this particular use case, but I don't think I can justify deviating from the typing spec to change the behavior. I'm therefore going to close this issue.

@erictraut erictraut added the as designed Not a bug, working as intended label Jan 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
as designed Not a bug, working as intended bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants