Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

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

Table of Contents

Important

This PEP is a historical document: seeSelf andtyping.Self for up-to-date specs and documentation. Canonical typing specs are maintained at thetyping specs site; runtime typing behaviour is described in the CPython documentation.

×

See thetyping specification update process for how to propose changes to the typing spec.

Abstract

This PEP introduces a simple and intuitive way to annotate methods that returnan instance of their class. This behaves the same as theTypeVar-basedapproach specified inPEP 484but is more concise and easier to follow.

Motivation

A common use case is to write a method that returns an instance of the sameclass, usually by returningself.

classShape:defset_scale(self,scale:float):self.scale=scalereturnselfShape().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=scalereturnselfShape().set_scale(0.5)# => Shape

However, when we callset_scale on a subclass ofShape, the typechecker still infers the return type to beShape. This is problematic insituations such as the one shown below, where the type checker will return anerror because we are trying to use attributes or methods not present on thebase class.

classCircle(Shape):defset_radius(self,r:float)->Circle:self.radius=rreturnselfCircle().set_scale(0.5)# *Shape*, not CircleCircle().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 thebase class as the bound and use it as the annotation for theselfparameter and the return type:

fromtypingimportTypeVarTShape=TypeVar("TShape",bound="Shape")classShape:defset_scale(self:TShape,scale:float)->TShape:self.scale=scalereturnselfclassCircle(Shape):defset_radius(self,radius:float)->Circle:self.radius=radiusreturnselfCircle().set_scale(0.5).set_radius(2.7)# => Circle

Unfortunately, this is verbose and unintuitive. Becauseself is usuallynot 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 thebound on theTypeVar(bound="Shape") or the annotation forself.

This difficulty means that users often give up and either use fallback typeslikeAny or just omit the type annotation completely, both of which makethe code less safe.

We propose a more intuitive and succinct way of expressing the aboveintention. We introduce a special formSelf that stands for a typevariable bound to the encapsulating class. For situations such as the oneabove, the user simply has to annotate the return type asSelf:

fromtypingimportSelfclassShape:defset_scale(self,scale:float)->Self:self.scale=scalereturnselfclassCircle(Shape):defset_radius(self,radius:float)->Self:self.radius=radiusreturnself

By annotating the return type asSelf, we no longer have to declare aTypeVar with an explicit bound on the base class. The return typeSelfmirrors the fact that the function returnsself and is easier tounderstand.

As in the above example, the type checker will correctly infer the type ofCircle().set_scale(0.5) to beCircle, as expected.

Usage statistics

Weanalyzed popularopen-source projects and found that patterns like the above were used about40% as often as popular types likedict orCallable. For example,in typeshed alone, such “Self” types are used 523 times, compared to 1286 usesofdict and 1314 uses ofCallableas of October 2021.This suggests that aSelf type will be used quite often and users willbenefit a lot from the simpler approach above.

Users of Python types have also frequently requested this feature,both on theproposal docand onGitHub.

Specification

Use in Method Signatures

Self used in the signature of a method is treated as if it were aTypeVar bound to the class.

fromtypingimportSelfclassShape:defset_scale(self,scale:float)->Self:self.scale=scalereturnself

is treated equivalently to:

fromtypingimportTypeVarSelfShape=TypeVar("SelfShape",bound="Shape")classShape:defset_scale(self:SelfShape,scale:float)->SelfShape:self.scale=scalereturnself

This works the same for a subclass too:

classCircle(Shape):defset_radius(self,radius:float)->Self:self.radius=radiusreturnself

which is treated equivalently to:

SelfCircle=TypeVar("SelfCircle",bound="Circle")classCircle(Shape):defset_radius(self:SelfCircle,radius:float)->SelfCircle:self.radius=radiusreturnself

One implementation strategy is to simply desugar the former to the latter in apreprocessing step. If a method usesSelf in its signature, the type ofself within a method will beSelf. In other cases, the type ofself will remain the enclosing class.

Use in Classmethod Signatures

TheSelf type annotation is also useful for classmethods that returnan instance of the class that they operate on. For example,from_config inthe following snippet builds aShape object from a givenconfig.

classShape:def__init__(self,scale:float)->None:...@classmethoddeffrom_config(cls,config:dict[str,float])->Shape:returncls(config["scale"])

However, this means thatCircle.from_config(...) is inferred to return avalue of typeShape, when in fact it should beCircle:

classCircle(Shape):defcircumference(self)->float:...shape=Shape.from_config({"scale":7.0})# => Shapecircle=Circle.from_config({"scale":7.0})# => *Shape*, not Circlecircle.circumference()# Error: `Shape` has no attribute `circumference`

The current workaround for this is unintuitive and error-prone:

Self=TypeVar("Self",bound="Shape")classShape:@classmethoddeffrom_config(cls:type[Self],config:dict[str,float])->Self:returncls(config["scale"])

We propose usingSelf directly:

fromtypingimportSelfclassShape:@classmethoddeffrom_config(cls,config:dict[str,float])->Self:returncls(config["scale"])

This avoids the complicatedcls:type[Self] annotation and theTypeVardeclaration with abound. Once again, the latter code behaves equivalentlyto the former code.

Use in Parameter Types

Another use forSelf is to annotate parameters that expect instances ofthe 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:

fromtypingimportSelfclassShape:defdifference(self,other:Self)->float:...defapply(self,f:Callable[[Self],None])->None:...

Note that specifyingself:Self is harmless, so some users may find itmore 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 wehave aLinkedList whose elements must be subclasses of the current class.

fromdataclassesimportdataclassfromtypingimportGeneric,TypeVarT=TypeVar("T")@dataclassclassLinkedList(Generic[T]):value:Tnext:LinkedList[T]|None=None# OKLinkedList[int](value=1,next=LinkedList[int](value=2))# Not OKLinkedList[int](value=1,next=LinkedList[str](value="hello"))

However, annotating thenext attribute asLinkedList[T] allows invalidconstructions with subclasses:

@dataclassclassOrdinalLinkedList(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@dataclassclassLinkedList(Generic[T]):value:Tnext:Self|None=None@dataclassclassOrdinalLinkedList(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)# OKxs.next=LinkedList[int](value=3,next=None)# Not OK

The code above is semantically equivalent to treating each attributecontaining aSelf type as aproperty that returns that type:

fromdataclassesimportdataclassfromtypingimportAny,Generic,TypeVarT=TypeVar("T")Self=TypeVar("Self",bound="LinkedList")classLinkedList(Generic[T]):value:T@propertydefnext(self:Self)->Self|None:returnself._next@next.setterdefnext(self:Self,next:Self|None)->None:self._next=nextclassOrdinalLinkedList(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:Tdefset_value(self,value:T)->Self:...

This is equivalent to writing:

Self=TypeVar("Self",bound="Container[Any]")classContainer(Generic[T]):value:Tdefset_value(self:Self,value:T)->Self:...

The behavior is to preserve the type argument of the object on which themethod was called. When called on an object with concrete typeContainer[int],Self is bound toContainer[int]. When called withan object of generic typeContainer[T],Self is bound toContainer[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 methodset_value. Some type checkers may choose to implementSelf types usingclass-local type variables withSelf=TypeVar(“Self”,bound=Container[T]), which will infer a precise typeT. However, giventhat class-local type variables are not a standardized type system feature, itis also acceptable to inferAny forself.value. We leave this up tothe type checker.

Note that we reject usingSelf with type arguments, such asSelf[int].This is because it creates ambiguity about the type of theself parameterand 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,SelfclassShapeProtocol(Protocol):scale:floatdefset_scale(self,scale:float)->Self:self.scale=scalereturnself

is treated equivalently to:

fromtypingimportTypeVarSelfShape=TypeVar("SelfShape",bound="ShapeProtocol")classShapeProtocol(Protocol):scale:floatdefset_scale(self:SelfShape,scale:float)->SelfShape:self.scale=scalereturnself

SeePEP 544 fordetails on the behavior of TypeVars bound to protocols.

Checking a class for compatibility with a protocol: If a protocol usesSelf in methods or attribute annotations, then a classFoo isconsidered compatible with the protocol if its corresponding methods andattribute annotations use eitherSelf orFoo or any ofFoo’ssubclasses. See the examples below:

fromtypingimportProtocolclassShapeProtocol(Protocol):defset_scale(self,scale:float)->Self:...classReturnSelf:scale:float=1.0defset_scale(self,scale:float)->Self:self.scale=scalereturnselfclassReturnConcreteShape:scale:float=1.0defset_scale(self,scale:float)->ReturnConcreteShape:self.scale=scalereturnselfclassBadReturnType:scale:float=1.0defset_scale(self,scale:float)->int:self.scale=scalereturn42classReturnDifferentClass:scale:float=1.0defset_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:ReturnSelfreturn_concrete_shape:ReturnConcreteShapebad_return_type:BadReturnTypereturn_different_class:ReturnDifferentClassaccepts_shape(return_self_shape)# OKaccepts_shape(return_concrete_shape)# OKaccepts_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 referto the encapsulating class. In contexts involving nested classes,Selfwill always refer to the innermost class.

The following uses ofSelf are accepted:

classReturnsSelf:deffoo(self)->Self:...# Accepted@classmethoddefbar(cls)->Self:# Acceptedreturncls()def__new__(cls,value:int)->Self:...# Accepteddefexplicitly_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)@classmethoddefreturn_cls(cls)->type[Self]:returnclsclassChild(ReturnsSelf):# Accepted (we can override a method that uses Self annotations)deffoo(self)->Self:...classTakesSelf:deffoo(self,other:Self)->bool:...# AcceptedclassRecursive:# Accepted (treated as an @property returning ``Self | None``)next:Self|NoneclassCallableAttribute:deffoo(self)->int:...# Accepted (treated as an @property returning the Callable type)bar:Callable[[Self],int]=fooclassHasNestedFunction:x:int=42deffoo(self)->None:# Accepted (Self is bound to HasNestedFunction).defnested(z:int,inner_self:Self)->Self:print(z)print(inner_self.x)returninner_selfnested(42,self)# OKclassOuter: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=42defchild_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_valueclassBar(Generic[T]):defbar(self)->T:...classBaz(Bar[Self]):...# Rejected

We reject type aliases containingSelf. SupportingSelfoutside class definitions can require a lot of special-handling intype checkers. Given that it also goes against the rest of the PEP touseSelf outside a class definition, we believe the addedconvenience of aliases is not worth it:

TupleSelf=Tuple[Self,Self]# RejectedclassAlias:defreturn_tuple(self)->TupleSelf:# Rejectedreturn(self,self)

Note that we rejectSelf in staticmethods.Self does not add muchvalue since there is noself orcls to return. The only possible usecases would be to return a parameter itself or some element from a containerpassed in as a parameter. These don’t seem worth the additional complexity.

classBase:@staticmethoddefmake()->Self:# Rejected...@staticmethoddefreturn_parameter(foo:Self)->Self:# Rejected...

Likewise, we rejectSelf in metaclasses.Self in this PEP consistentlyrefers to the same type (that ofself). But in metaclasses, it would haveto refer to different types in different method signatures. For example, in__mul__,Self in the return type would refer to the implementing classFoo, not the enclosing classMyMetaclass. But, in__new__,Selfin the return type would refer to the enclosing classMyMetaclass. Toavoid confusion, we reject this edge case.

classMyMetaclass(type):def__new__(cls,*args:Any)->Self:# Rejectedreturnsuper().__new__(cls,*args)def__mul__(cls,count:int)->list[Self]:# Rejectedreturn[cls()]*countclassFoo(metaclass=MyMetaclass):...

Runtime behavior

BecauseSelf is not subscriptable, we propose an implementation similar totyping.NoReturn.

@_SpecialFormdefSelf(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 checkerinfer from the body of the method that the return type must be the same as thetype of theself parameter:

classShape:defset_scale(self,scale:float):self.scale=scalereturnself# Type checker infers that we are returning self

We reject this because Explicit Is Better Than Implicit. Beyond that, theabove approach will fail for type stubs, which don’t have method bodies toanalyze.

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 oranother way to spell “type of self”. However, the approach ultimately takenthere was the boundedTypeVar approach shown in our “before” examples.Other issues that discuss this includeMypy issue #2354 - Self types in genericclasses.

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:

Thanks to the following people for their feedback on the PEP:

Jia Chen, Rebecca Chen, Sergei Lebedev, Kaylynn Morgan, TuomasSuutari, Eric Traut, Alex Waygood, Shannon Zhu, and Никита Соболев

Copyright

This document is placed in the public domain or under theCC0-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


[8]ページ先頭

©2009-2025 Movatter.jp