PEP 673 – Self Type
- Author:
- Pradeep Kumar Srinivasan <gohanpra at gmail.com>, James Hilton-Balfe <gobot1234yt at gmail.com>
- Sponsor:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Typing-SIG list
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 10-Nov-2021
- Python-Version:
- 3.11
- Post-History:
- 17-Nov-2021
- Resolution:
- Python-Dev thread
Abstract
This PEP introduces a simple and intuitive way to annotate methods that return
an instance of their class. This behaves the same as theTypeVar
-based
approach specified inPEP 484
but is more concise and easier to follow.
Motivation
A common use case is to write a method that returns an instance of the same
class, usually by returningself
.
classShape:
defset_scale(self,scale:float):
self.scale=scale
returnself
Shape().set_scale(0.5)# => should be Shape
One way to denote the return type is to specify it as the current class, say,
Shape
.Using the method makes the type checker infer the typeShape
,
as expected.
classShape:
defset_scale(self,scale:float)->Shape:
self.scale=scale
returnself
Shape().set_scale(0.5)# => Shape
However, when we callset_scale
on a subclass ofShape
,the type
checker still infers the return type to beShape
.This is problematic in
situations such as the one shown below, where the type checker will return an
error because we are trying to use attributes or methods not present on the
base class.
classCircle(Shape):
defset_radius(self,r:float)->Circle:
self.radius=r
returnself
Circle().set_scale(0.5)# *Shape*, not Circle
Circle().set_scale(0.5).set_radius(2.7)
# => Error: Shape has no attribute set_radius
The present workaround for such instances is to define aTypeVar
with the
base class as the bound and use it as the annotation for theself
parameter and the return type:
fromtypingimportTypeVar
TShape=TypeVar("TShape",bound="Shape")
classShape:
defset_scale(self:TShape,scale:float)->TShape:
self.scale=scale
returnself
classCircle(Shape):
defset_radius(self,radius:float)->Circle:
self.radius=radius
returnself
Circle().set_scale(0.5).set_radius(2.7)# => Circle
Unfortunately, this is verbose and unintuitive. Becauseself
is usually
not explicitly annotated, the above solution doesn’t immediately come to mind,
and even if it does, it is very easy to go wrong by forgetting either the
bound on theTypeVar(bound= "Shape" )
or the annotation forself
.
This difficulty means that users often give up and either use fallback types
likeAny
or just omit the type annotation completely, both of which make
the code less safe.
We propose a more intuitive and succinct way of expressing the above
intention. We introduce a special formSelf
that stands for a type
variable bound to the encapsulating class. For situations such as the one
above, the user simply has to annotate the return type asSelf
:
fromtypingimportSelf
classShape:
defset_scale(self,scale:float)->Self:
self.scale=scale
returnself
classCircle(Shape):
defset_radius(self,radius:float)->Self:
self.radius=radius
returnself
By annotating the return type asSelf
,we no longer have to declare a
TypeVar
with an explicit bound on the base class. The return typeSelf
mirrors the fact that the function returnsself
and is easier to
understand.
As in the above example, the type checker will correctly infer the type of
Circle().set_scale(0.5)
to beCircle
,as expected.
Usage statistics
Weanalyzedpopular
open-source projects and found that patterns like the above were used about
40%as often as popular types likedict
orCallable
.For example,
in typeshed alone, such “Self” types are used 523 times, compared to 1286 uses
ofdict
and 1314 uses ofCallable
as of October 2021.
This suggests that aSelf
type will be used quite often and users will
benefit a lot from the simpler approach above.
Users of Python types have also frequently requested this feature, both on theproposal doc and onGitHub.
Specification
Use in Method Signatures
Self
used in the signature of a method is treated as if it were a
TypeVar
bound to the class.
fromtypingimportSelf
classShape:
defset_scale(self,scale:float)->Self:
self.scale=scale
returnself
is treated equivalently to:
fromtypingimportTypeVar
SelfShape=TypeVar("SelfShape",bound="Shape")
classShape:
defset_scale(self:SelfShape,scale:float)->SelfShape:
self.scale=scale
returnself
This works the same for a subclass too:
classCircle(Shape):
defset_radius(self,radius:float)->Self:
self.radius=radius
returnself
which is treated equivalently to:
SelfCircle=TypeVar("SelfCircle",bound="Circle")
classCircle(Shape):
defset_radius(self:SelfCircle,radius:float)->SelfCircle:
self.radius=radius
returnself
One implementation strategy is to simply desugar the former to the latter in a
preprocessing step. If a method usesSelf
in its signature, the type of
self
within a method will beSelf
.In other cases, the type of
self
will remain the enclosing class.
Use in Classmethod Signatures
TheSelf
type annotation is also useful for classmethods that return
an instance of the class that they operate on. For example,from_config
in
the following snippet builds aShape
object from a givenconfig
.
classShape:
def__init__(self,scale:float)->None:...
@classmethod
deffrom_config(cls,config:dict[str,float])->Shape:
returncls(config["scale"])
However, this means thatCircle.from_config(...)
is inferred to return a
value of typeShape
,when in fact it should beCircle
:
classCircle(Shape):
defcircumference(self)->float:...
shape=Shape.from_config({"scale":7.0})
# => Shape
circle=Circle.from_config({"scale":7.0})
# => *Shape*, not Circle
circle.circumference()
# Error: `Shape` has no attribute `circumference`
The current workaround for this is unintuitive and error-prone:
Self=TypeVar("Self",bound="Shape")
classShape:
@classmethod
deffrom_config(
cls:type[Self],config:dict[str,float]
)->Self:
returncls(config["scale"])
We propose usingSelf
directly:
fromtypingimportSelf
classShape:
@classmethod
deffrom_config(cls,config:dict[str,float])->Self:
returncls(config["scale"])
This avoids the complicatedcls:type[Self]
annotation and theTypeVar
declaration with abound
.Once again, the latter code behaves equivalently
to the former code.
Use in Parameter Types
Another use forSelf
is to annotate parameters that expect instances of
the current class:
Self=TypeVar("Self",bound="Shape")
classShape:
defdifference(self:Self,other:Self)->float:...
defapply(self:Self,f:Callable[[Self],None])->None:...
We propose usingSelf
directly to achieve the same behavior:
fromtypingimportSelf
classShape:
defdifference(self,other:Self)->float:...
defapply(self,f:Callable[[Self],None])->None:...
Note that specifyingself:Self
is harmless, so some users may find it
more readable to write the above as:
classShape:
defdifference(self:Self,other:Self)->float:...
Use in Attribute Annotations
Another use forSelf
is to annotate attributes. One example is where we
have aLinkedList
whose elements must be subclasses of the current class.
fromdataclassesimportdataclass
fromtypingimportGeneric,TypeVar
T=TypeVar("T")
@dataclass
classLinkedList(Generic[T]):
value:T
next:LinkedList[T]|None=None
# OK
LinkedList[int](value=1,next=LinkedList[int](value=2))
# Not OK
LinkedList[int](value=1,next=LinkedList[str](value="hello"))
However, annotating thenext
attribute asLinkedList[T]
allows invalid
constructions with subclasses:
@dataclass
classOrdinalLinkedList(LinkedList[int]):
defordinal_value(self)->str:
returnas_ordinal(self.value)
# Should not be OK because LinkedList[int] is not a subclass of
# OrdinalLinkedList, # but the type checker allows it.
xs=OrdinalLinkedList(value=1,next=LinkedList[int](value=2))
ifxs.next:
print(xs.next.ordinal_value())# Runtime Error.
We propose expressing this constraint usingnext:Self|None
:
fromtypingimportSelf
@dataclass
classLinkedList(Generic[T]):
value:T
next:Self|None=None
@dataclass
classOrdinalLinkedList(LinkedList[int]):
defordinal_value(self)->str:
returnas_ordinal(self.value)
xs=OrdinalLinkedList(value=1,next=LinkedList[int](value=2))
# Type error: Expected OrdinalLinkedList, got LinkedList[int].
ifxs.nextisnotNone:
xs.next=OrdinalLinkedList(value=3,next=None)# OK
xs.next=LinkedList[int](value=3,next=None)# Not OK
The code above is semantically equivalent to treating each attribute
containing aSelf
type as aproperty
that returns that type:
fromdataclassesimportdataclass
fromtypingimportAny,Generic,TypeVar
T=TypeVar("T")
Self=TypeVar("Self",bound="LinkedList")
classLinkedList(Generic[T]):
value:T
@property
defnext(self:Self)->Self|None:
returnself._next
@next.setter
defnext(self:Self,next:Self|None)->None:
self._next=next
classOrdinalLinkedList(LinkedList[int]):
defordinal_value(self)->str:
returnstr(self.value)
Use in Generic Classes
Self
can also be used in generic class methods:
classContainer(Generic[T]):
value:T
defset_value(self,value:T)->Self:...
This is equivalent to writing:
Self=TypeVar("Self",bound="Container[Any]")
classContainer(Generic[T]):
value:T
defset_value(self:Self,value:T)->Self:...
The behavior is to preserve the type argument of the object on which the
method was called. When called on an object with concrete type
Container[int]
,Self
is bound toContainer[int]
.When called with
an object of generic typeContainer[T]
,Self
is bound to
Container[T]
:
defobject_with_concrete_type()->None:
int_container:Container[int]
str_container:Container[str]
reveal_type(int_container.set_value(42))# => Container[int]
reveal_type(str_container.set_value("hello"))# => Container[str]
defobject_with_generic_type(
container:Container[T],value:T,
)->Container[T]:
returncontainer.set_value(value)# => Container[T]
The PEP doesn’t specify the exact type ofself.value
within the method
set_value
.Some type checkers may choose to implementSelf
types using
class-local type variables withSelf=TypeVar( “Self”,
bound=Container[T])
,which will infer a precise typeT
.However, given
that class-local type variables are not a standardized type system feature, it
is also acceptable to inferAny
forself.value
.We leave this up to
the type checker.
Note that we reject usingSelf
with type arguments, such asSelf[int]
.
This is because it creates ambiguity about the type of theself
parameter
and introduces unnecessary complexity:
classContainer(Generic[T]):
deffoo(
self,other:Self[int],other2:Self,
)->Self[str]:# Rejected
...
In such cases, we recommend using an explicit type forself
:
classContainer(Generic[T]):
deffoo(
self:Container[T],
other:Container[int],
other2:Container[T]
)->Container[str]:...
Use in Protocols
Self
is valid within Protocols, similar to its use in classes:
fromtypingimportProtocol,Self
classShapeProtocol(Protocol):
scale:float
defset_scale(self,scale:float)->Self:
self.scale=scale
returnself
is treated equivalently to:
fromtypingimportTypeVar
SelfShape=TypeVar("SelfShape",bound="ShapeProtocol")
classShapeProtocol(Protocol):
scale:float
defset_scale(self:SelfShape,scale:float)->SelfShape:
self.scale=scale
returnself
SeePEP 544for details on the behavior of TypeVars bound to protocols.
Checking a class for compatibility with a protocol: If a protocol uses
Self
in methods or attribute annotations, then a classFoo
is
considered compatible with the protocol if its corresponding methods and
attribute annotations use eitherSelf
orFoo
or any ofFoo
’s
subclasses. See the examples below:
fromtypingimportProtocol
classShapeProtocol(Protocol):
defset_scale(self,scale:float)->Self:...
classReturnSelf:
scale:float=1.0
defset_scale(self,scale:float)->Self:
self.scale=scale
returnself
classReturnConcreteShape:
scale:float=1.0
defset_scale(self,scale:float)->ReturnConcreteShape:
self.scale=scale
returnself
classBadReturnType:
scale:float=1.0
defset_scale(self,scale:float)->int:
self.scale=scale
return42
classReturnDifferentClass:
scale:float=1.0
defset_scale(self,scale:float)->ReturnConcreteShape:
returnReturnConcreteShape(...)
defaccepts_shape(shape:ShapeProtocol)->None:
y=shape.set_scale(0.5)
reveal_type(y)
defmain()->None:
return_self_shape:ReturnSelf
return_concrete_shape:ReturnConcreteShape
bad_return_type:BadReturnType
return_different_class:ReturnDifferentClass
accepts_shape(return_self_shape)# OK
accepts_shape(return_concrete_shape)# OK
accepts_shape(bad_return_type)# Not OK
# Not OK because it returns a non-subclass.
accepts_shape(return_different_class)
Valid Locations forSelf
ASelf
annotation is only valid in class contexts, and will always refer
to the encapsulating class. In contexts involving nested classes,Self
will always refer to the innermost class.
The following uses ofSelf
are accepted:
classReturnsSelf:
deffoo(self)->Self:...# Accepted
@classmethod
defbar(cls)->Self:# Accepted
returncls()
def__new__(cls,value:int)->Self:...# Accepted
defexplicitly_use_self(self:Self)->Self:...# Accepted
# Accepted (Self can be nested within other types)
defreturns_list(self)->list[Self]:...
# Accepted (Self can be nested within other types)
@classmethod
defreturn_cls(cls)->type[Self]:
returncls
classChild(ReturnsSelf):
# Accepted (we can override a method that uses Self annotations)
deffoo(self)->Self:...
classTakesSelf:
deffoo(self,other:Self)->bool:...# Accepted
classRecursive:
# Accepted (treated as an @property returning ``Self | None``)
next:Self|None
classCallableAttribute:
deffoo(self)->int:...
# Accepted (treated as an @property returning the Callable type)
bar:Callable[[Self],int]=foo
classHasNestedFunction:
x:int=42
deffoo(self)->None:
# Accepted (Self is bound to HasNestedFunction).
defnested(z:int,inner_self:Self)->Self:
print(z)
print(inner_self.x)
returninner_self
nested(42,self)# OK
classOuter:
classInner:
deffoo(self)->Self:...# Accepted (Self is bound to Inner)
The following uses ofSelf
are rejected.
deffoo(bar:Self)->Self:...# Rejected (not within a class)
bar:Self# Rejected (not within a class)
classFoo:
# Rejected (Self is treated as unknown).
defhas_existing_self_annotation(self:T)->Self:...
classFoo:
defreturn_concrete_type(self)->Self:
returnFoo()# Rejected (see FooChild below for rationale)
classFooChild(Foo):
child_value:int=42
defchild_method(self)->None:
# At runtime, this would be Foo, not FooChild.
y=self.return_concrete_type()
y.child_value
# Runtime error: Foo has no attribute child_value
classBar(Generic[T]):
defbar(self)->T:...
classBaz(Bar[Self]):...# Rejected
We reject type aliases containingSelf
.SupportingSelf
outside class definitions can require a lot of special-handling in
type checkers. Given that it also goes against the rest of the PEP to
useSelf
outside a class definition, we believe the added
convenience of aliases is not worth it:
TupleSelf=Tuple[Self,Self]# Rejected
classAlias:
defreturn_tuple(self)->TupleSelf:# Rejected
return(self,self)
Note that we rejectSelf
in staticmethods.Self
does not add much
value since there is noself
orcls
to return. The only possible use
cases would be to return a parameter itself or some element from a container
passed in as a parameter. These don’t seem worth the additional complexity.
classBase:
@staticmethod
defmake()->Self:# Rejected
...
@staticmethod
defreturn_parameter(foo:Self)->Self:# Rejected
...
Likewise, we rejectSelf
in metaclasses.Self
in this PEP consistently
refers to the same type (that ofself
). But in metaclasses, it would have
to refer to different types in different method signatures. For example, in
__mul__
,Self
in the return type would refer to the implementing class
Foo
,not the enclosing classMyMetaclass
.But, in__new__
,Self
in the return type would refer to the enclosing classMyMetaclass
.To
avoid confusion, we reject this edge case.
classMyMetaclass(type):
def__new__(cls,*args:Any)->Self:# Rejected
returnsuper().__new__(cls,*args)
def__mul__(cls,count:int)->list[Self]:# Rejected
return[cls()]*count
classFoo(metaclass=MyMetaclass):...
Runtime behavior
BecauseSelf
is not subscriptable, we propose an implementation similar to
typing.NoReturn
.
@_SpecialForm
defSelf(self,params):
"""Used to spell the type of" self "in classes.
Example::
from typing import Self
class ReturnsSelf:
def parse(self, data: bytes) -> Self:
...
return self
"""
raiseTypeError(f"{self}is not subscriptable ")
Rejected Alternatives
Allow the Type Checker to Infer the Return Type
One proposal is to leave theSelf
type implicit and let the type checker
infer from the body of the method that the return type must be the same as the
type of theself
parameter:
classShape:
defset_scale(self,scale:float):
self.scale=scale
returnself# Type checker infers that we are returning self
We reject this because Explicit Is Better Than Implicit. Beyond that, the above approach will fail for type stubs, which don’t have method bodies to analyze.
Reference Implementations
Mypy: Proof of concept implementation inMypy.
Pyright: v1.1.184
Runtime implementation ofSelf
:PR.
Resources
Similar discussions on aSelf
type in Python started in Mypy around 2016:
Mypy issue #1212- SelfType or
another way to spell “type of self”. However, the approach ultimately taken
there was the boundedTypeVar
approach shown in our “before” examples.
Other issues that discuss this includeMypy issue #2354- Self types in generic
classes.
- Pradeep made a concrete proposal at the PyCon Typing Summit 2021:
- recorded talk,slides.
James brought up the proposal independently on typing-sig: Typing-sig thread.
Other languages have similar ways to express the type of the enclosing class:
- TypeScript has the
this
type (TypeScript docs) - Rust has the
Self
type (Rust docs)
Thanks to the following people for their feedback on the PEP:
Jia Chen, Rebecca Chen, Sergei Lebedev, Kaylynn Morgan, Tuomas Suutari, Eric Traut, Alex Waygood, Shannon Zhu, and Никита Соболев
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0673.rst
Last modified:2024-06-11 22:12:09 GMT