Skip to content

Convert type[Self] to callable from class constructor - #5592

Closed
crypt0lith wants to merge 2 commits into
pyrefly-org:mainfrom
crypt0lith:fix/self-type-as-callable
Closed

crypt0lith wants to merge 2 commits into
pyrefly-org:mainfrom
crypt0lith:fix/self-type-as-callable

Conversation

@crypt0lith

@crypt0lith crypt0lith commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Converting type[Self], such as cls in a classmethod, or type[T] for a type variable T bound to a class, to a callable fell back to type.__call__, (*args: Any, **kwds: Any) -> Any. Passing cls to a ParamSpec-generic function lost its signature, and assigning it to an incompatible callable type was allowed.

Convert type[Self] and type[T] from the constructor of the class they are bound to. As in a call through them, change a constructor return that is an instance of the class to Self or T, so the converted callable still returns the subclass when the classmethod is inherited. Expand the solved T of __new__(cls: type[T]) -> T before checking whether its return is an instance of the class.

Fixes meta#5591

  • I am a human, and I fully understand the code I am submitting.

Test Plan

cargo test -p pyrefly --lib test::constructors::test_type_of_self_as_callable

@meta-codesync

meta-codesync Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

This pull request has been imported. If you are a Meta employee, you can view this in D130651720. (Because this pull request was imported automatically, there will not be any future comments.)

@stroxler stroxler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I think working on a class-constructor-to-callable hook makes sense.

But it looks like the current behavior has some gaps. Consider this code, which uses a type variable rather than typing.Self:

from typing import Callable, Self, TypeVar, reveal_type

def relay[**P, R](f: Callable[P, R]) -> Callable[P, R]:
    return f

class C:
    def __new__[S: C](cls: type[S], x: int) -> S:
        raise NotImplementedError
    @classmethod
    def make(cls) -> Self:
        reveal_type(relay(cls))
        return relay(cls)(1)

_S = TypeVar('_S', bound='Legacy')
class Legacy:
    def __new__(cls: type[_S], x: int) -> _S:
        raise NotImplementedError
    @classmethod
    def make(cls) -> Self:
        reveal_type(relay(cls))
        return relay(cls)(1)

class D(C): pass
reveal_type(D.make())

it appears that this change causes a regression here; trunk accepts this, as do other type checkers, but with this diff we get

 INFO typevar-new.py:11:20-32: revealed type: (x: int) -> C [reveal-type]
ERROR typevar-new.py:12:16-29: Returned type `C` is not assignable to declared return type `Self@C` [bad-return]
 INFO typevar-new.py:20:20-32: revealed type: (x: int) -> Legacy [reveal-type]
ERROR typevar-new.py:21:16-29: Returned type `Legacy` is not assignable to declared return type `Self@Legacy` [bad-return]
 INFO typevar-new.py:24:12-22: revealed type: D [reveal-type]
 INFO 2 errors

We need to work out why the current adapter causes this; I suspect we're making too many assumptions about Self here to handle other ways of annotating cls(?)

An AI reviewer suggests suggests this as a potential fix: expand solved constructor return variables before the compatibility check, then add regression tests.

Converting type[Self], such as 'cls' in a classmethod, to a callable fell
back to 'type.__call__', '(*args: Any, **kwds: Any) -> Any'. Passing 'cls'
to a ParamSpec-generic function lost its signature, and assigning it to an
incompatible callable type was allowed.

Convert type[Self] from the constructor of the class bound to Self. As in
a call through type[Self], change a constructor return that is an instance
of the class to Self, so the converted callable still returns the subclass
when the classmethod is inherited.

Fixes pyrefly-org#5591
@crypt0lith
crypt0lith force-pushed the fix/self-type-as-callable branch from d864692 to 905e2d5 Compare November 11, 2026 13:50
@github-actions github-actions Bot added size/s and removed size/s labels Nov 11, 2026

@stroxler stroxler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

@meta-codesync

meta-codesync Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

This pull request has been merged in 70dcfe7.

@crypt0lith
crypt0lith deleted the fix/self-type-as-callable branch November 13, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

type[Self] converted to a callable drops constructor signature

3 participants