
## Summary closes https://github.com/astral-sh/ty/issues/1333 ## Test Plan Regression test
12 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)
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
Type of (unannotated) self
parameters
In instance methods, the first parameter (regardless of its name) is assumed to have the type
typing.Self
, unless it has an explicit annotation. This does not apply to @classmethod
and
@staticmethod
s.
[environment]
python-version = "3.11"
from typing import Self
class A:
def implicit_self(self) -> Self:
# TODO: This should be Self@implicit_self
reveal_type(self) # revealed: Unknown
return self
def a_method(self) -> int:
def first_arg_is_not_self(a: int) -> int:
reveal_type(a) # revealed: int
return a
return first_arg_is_not_self(1)
@classmethod
def a_classmethod(cls) -> Self:
# TODO: This should be type[Self@bar]
reveal_type(cls) # revealed: Unknown
return cls()
@staticmethod
def a_staticmethod(x: int): ...
a = A()
reveal_type(a.implicit_self()) # revealed: A
reveal_type(a.implicit_self) # revealed: bound method A.implicit_self() -> A
Calling an instance method explicitly verifies the first argument:
A.implicit_self(a)
# error: [invalid-argument-type] "Argument to function `implicit_self` is incorrect: Argument type `Literal[1]` does not satisfy upper bound `A` of type variable `Self`"
A.implicit_self(1)
Passing self
implicitly also verifies the type:
from typing import Never
class Strange:
def can_not_be_called(self: Never) -> None: ...
# error: [invalid-argument-type] "Argument to bound method `can_not_be_called` is incorrect: Expected `Never`, found `Strange`"
Strange().can_not_be_called()
If the method is a class or static method then first argument is not inferred as Self
:
A.a_classmethod()
A.a_classmethod(a) # error: [too-many-positional-arguments]
A.a_staticmethod(1)
a.a_staticmethod(1)
A.a_staticmethod(a) # error: [invalid-argument-type]
The first parameter of instance methods always has type Self
, if it is not explicitly annotated.
The name self
is not special in any way.
class B:
def name_does_not_matter(this) -> Self:
# TODO: Should reveal Self@name_does_not_matter
reveal_type(this) # revealed: Unknown
return this
def positional_only(self, /, x: int) -> Self:
# TODO: Should reveal Self@positional_only
reveal_type(self) # revealed: Unknown
return self
def keyword_only(self, *, x: int) -> Self:
# TODO: Should reveal Self@keyword_only
reveal_type(self) # revealed: Unknown
return self
@property
def a_property(self) -> Self:
# TODO: Should reveal Self@a_property
reveal_type(self) # revealed: Unknown
return self
reveal_type(B().name_does_not_matter()) # revealed: B
reveal_type(B().positional_only(1)) # revealed: B
reveal_type(B().keyword_only(x=1)) # revealed: B
# TODO: this should be B
reveal_type(B().a_property) # revealed: Unknown
This also works for generic classes:
from typing import Self, Generic, TypeVar
T = TypeVar("T")
class G(Generic[T]):
def id(self) -> Self:
# TODO: Should reveal Self@id
reveal_type(self) # revealed: Unknown
return self
reveal_type(G[int]().id()) # revealed: G[int]
reveal_type(G[str]().id()) # revealed: G[str]
Free functions and nested functions do not use implicit Self
:
def not_a_method(self):
reveal_type(self) # revealed: Unknown
# error: [invalid-type-form]
def does_not_return_self(self) -> Self:
return self
class C:
def outer(self) -> None:
def inner(self):
reveal_type(self) # revealed: Unknown
reveal_type(not_a_method) # revealed: def not_a_method(self) -> Unknown
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]
Implicit self for classes with a default value for their generic parameter
from typing import Self, TypeVar, Generic
class Container[T = bytes]:
def method(self) -> Self:
return self
def _(c: Container[str], d: Container):
reveal_type(c.method()) # revealed: Container[str]
reveal_type(d.method()) # revealed: Container[bytes]
T = TypeVar("T", default=bytes)
class LegacyContainer(Generic[T]):
def method(self) -> Self:
return self
def _(c: LegacyContainer[str], d: LegacyContainer):
reveal_type(c.method()) # revealed: LegacyContainer[str]
reveal_type(d.method()) # revealed: LegacyContainer[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)
Explicit annotations override implicit Self
If the first parameter is explicitly annotated, that annotation takes precedence over the implicit
Self
type.
[environment]
python-version = "3.12"
from __future__ import annotations
from typing import final
@final
class Disjoint: ...
class Explicit:
# TODO: We could emit a warning if the annotated type of `self` is disjoint from `Explicit`
def bad(self: Disjoint) -> None:
reveal_type(self) # revealed: Disjoint
def forward(self: Explicit) -> None:
reveal_type(self) # revealed: Explicit
# error: [invalid-argument-type] "Argument to bound method `bad` is incorrect: Expected `Disjoint`, found `Explicit`"
Explicit().bad()
Explicit().forward()
class ExplicitGeneric[T]:
def special(self: ExplicitGeneric[int]) -> None:
reveal_type(self) # revealed: ExplicitGeneric[int]
ExplicitGeneric[int]().special()
# TODO: this should be an `invalid-argument-type` error
ExplicitGeneric[str]().special()
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
Non-positional first parameters
This makes sure that we don't bind self
if it's not a positional parameter:
from ty_extensions import CallableTypeOf
class C:
def method(*args, **kwargs) -> None: ...
def _(c: CallableTypeOf[C().method]):
reveal_type(c) # revealed: (...) -> None