ruff/crates/ty_python_semantic/resources/mdtest/annotations/self.md
Shaygan Hooshyari 05622ae757
[ty] Bind Self typevar to method context (#20366)
Fixes: https://github.com/astral-sh/ty/issues/1173

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR will change the logic of binding Self type variables to bind
self to the immediate function that it's used on.
Since we are binding `self` to methods and not the class itself we need
to ensure that we bind self consistently.

The fix is to traverse scopes containing the self and find the first
function inside a class and use that function to bind the typevar for
self.

If no such scope is found we fallback to the normal behavior. Using Self
outside of a class scope is not legal anyway.

## Test Plan

Added a new mdtest.

Checked the diagnostics that are not emitted anymore in [primer
results](https://github.com/astral-sh/ruff/pull/20366#issuecomment-3289411424).
It looks good altough I don't completely understand what was wrong
before.

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
2025-09-17 14:58:54 -04:00

6.4 KiB

Self

[environment]
python-version = "3.13"

Self is treated as if it were a TypeVar bound to the class it's being used on.

typing.Self is only available in Python 3.11 and later.

Methods

from typing import Self

class Shape:
    def set_scale(self: Self, scale: float) -> Self:
        reveal_type(self)  # revealed: Self@set_scale
        return self

    def nested_type(self: Self) -> list[Self]:
        return [self]

    def nested_func(self: Self) -> Self:
        def inner() -> Self:
            reveal_type(self)  # revealed: Self@nested_func
            return self
        return inner()

    def nested_func_without_enclosing_binding(self):
        def inner(x: Self):
            reveal_type(x)  # revealed: Self@nested_func_without_enclosing_binding
        inner(self)

    def implicit_self(self) -> Self:
        # TODO: first argument in a method should be considered as "typing.Self"
        reveal_type(self)  # revealed: Unknown
        return self

reveal_type(Shape().nested_type())  # revealed: list[Shape]
reveal_type(Shape().nested_func())  # revealed: Shape

class Circle(Shape):
    def set_scale(self: Self, scale: float) -> Self:
        reveal_type(self)  # revealed: Self@set_scale
        return self

class Outer:
    class Inner:
        def foo(self: Self) -> Self:
            reveal_type(self)  # revealed: Self@foo
            return self

typing_extensions

[environment]
python-version = "3.10"
from typing_extensions import Self

class C:
    def method(self: Self) -> Self:
        return self

reveal_type(C().method())  # revealed: C

Class Methods

from typing import Self, TypeVar

class Shape:
    def foo(self: Self) -> Self:
        return self

    @classmethod
    def bar(cls: type[Self]) -> Self:
        # TODO: type[Shape]
        reveal_type(cls)  # revealed: @Todo(unsupported type[X] special form)
        return cls()

class Circle(Shape): ...

reveal_type(Shape().foo())  # revealed: Shape
# TODO: Shape
reveal_type(Shape.bar())  # revealed: Unknown

Attributes

TODO: The use of Self to annotate the next_node attribute should be modeled as a property, using Self in its parameter and return type.

from typing import Self

class LinkedList:
    value: int
    next_node: Self

    def next(self: Self) -> Self:
        reveal_type(self.value)  # revealed: int
        # TODO: no error
        # error: [invalid-return-type]
        return self.next_node

reveal_type(LinkedList().next())  # revealed: LinkedList

Generic Classes

from typing import Self, Generic, TypeVar

T = TypeVar("T")

class Container(Generic[T]):
    value: T
    def set_value(self: Self, value: T) -> Self:
        return self

int_container: Container[int] = Container[int]()
reveal_type(int_container)  # revealed: Container[int]
reveal_type(int_container.set_value(1))  # revealed: Container[int]

Protocols

TODO: https://typing.python.org/en/latest/spec/generics.html#use-in-protocols

Annotations

from typing import Self

class Shape:
    def union(self: Self, other: Self | None):
        reveal_type(other)  # revealed: Self@union | None
        return self

Self for classes with a default value for their generic parameter

This is a regression test for https://github.com/astral-sh/ty/issues/1156.

from typing import Self

class Container[T = bytes]:
    def __init__(self: Self, data: T | None = None) -> None:
        self.data = data

reveal_type(Container())  # revealed: Container[bytes]
reveal_type(Container(1))  # revealed: Container[int]
reveal_type(Container("a"))  # revealed: Container[str]
reveal_type(Container(b"a"))  # revealed: Container[bytes]

Invalid Usage

Self cannot be used in the signature of a function or variable.

from typing import Self, Generic, TypeVar

T = TypeVar("T")

# error: [invalid-type-form]
def x(s: Self): ...

# error: [invalid-type-form]
b: Self

# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self"
class Foo:
    # TODO: rejected Self because self has a different type
    def has_existing_self_annotation(self: T) -> Self:
        return self  # error: [invalid-return-type]

    def return_concrete_type(self) -> Self:
        # TODO: tell user to use "Foo" instead of "Self"
        # error: [invalid-return-type]
        return Foo()

    @staticmethod
    # TODO: reject because of staticmethod
    def make() -> Self:
        # error: [invalid-return-type]
        return Foo()

class Bar(Generic[T]):
    foo: T
    def bar(self) -> T:
        return self.foo

# error: [invalid-type-form]
class Baz(Bar[Self]): ...

class MyMetaclass(type):
    # TODO: rejected
    def __new__(cls) -> Self:
        return super().__new__(cls)

Binding a method fixes Self

When a method is bound, any instances of Self in its signature are "fixed", since we now know the specific type of the bound parameter.

from typing import Self

class C:
    def instance_method(self, other: Self) -> Self:
        return self

    @classmethod
    def class_method(cls) -> Self:
        return cls()

# revealed: bound method C.instance_method(other: C) -> C
reveal_type(C().instance_method)
# revealed: bound method <class 'C'>.class_method() -> C
reveal_type(C.class_method)

class D(C): ...

# revealed: bound method D.instance_method(other: D) -> D
reveal_type(D().instance_method)
# revealed: bound method <class 'D'>.class_method() -> D
reveal_type(D.class_method)

In nested functions self binds to the method. So in the following example the self in C.b is bound at C.f.

from typing import Self
from ty_extensions import generic_context

class C[T]():
    def f(self: Self):
        def b(x: Self):
            reveal_type(x)  # revealed: Self@f
        reveal_type(generic_context(b))  # revealed: None

reveal_type(generic_context(C.f))  # revealed: tuple[Self@f]

Even if the Self annotation appears first in the nested function, it is the method that binds Self.

from typing import Self
from ty_extensions import generic_context

class C:
    def f(self: "C"):
        def b(x: Self):
            reveal_type(x)  # revealed: Self@f
        reveal_type(generic_context(b))  # revealed: None

reveal_type(generic_context(C.f))  # revealed: None