Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 544 – Protocols: Structural subtyping (static duck typing)

Author:
Ivan Levkivskyi <levkivskyi at gmail.com>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, Łukasz Langa <lukasz at python.org>
BDFL-Delegate:
Guido van Rossum <guido at python.org>
Discussions-To:
Python-Dev list
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
05-Mar-2017
Python-Version:
3.8
Resolution:
Typing-SIG message

Table of Contents

Important

This PEP is a historical document: seeProtocols andtyping.Protocol 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

Type hints introduced inPEP 484 can be used to specify type metadatafor static type checkers and other third party tools. However,PEP 484only specifies the semantics ofnominal subtyping. In this PEP we specifystatic and runtime semantics of protocol classes that will provide a supportforstructural subtyping (static duck typing).

Rationale and Goals

Currently,PEP 484 and thetyping module[typing] define abstractbase classes for several common Python protocols such asIterable andSized. The problem with them is that a class has to be explicitly markedto support them, which is unpythonic and unlike what one wouldnormally do in idiomatic dynamically typed Python code. For example,this conforms toPEP 484:

fromtypingimportSized,Iterable,IteratorclassBucket(Sized,Iterable[int]):...def__len__(self)->int:...def__iter__(self)->Iterator[int]:...

The same problem appears with user-defined ABCs: they must be explicitlysubclassed or registered. This is particularly difficult to do with librarytypes as the type objects may be hidden deep in the implementationof the library. Also, extensive use of ABCs might impose additionalruntime costs.

The intention of this PEP is to solve all these problemsby allowing users to write the above code without explicit base classes inthe class definition, allowingBucket to be implicitly considereda subtype of bothSized andIterable[int] by static type checkersusing structural[wiki-structural] subtyping:

fromtypingimportIterator,IterableclassBucket:...def__len__(self)->int:...def__iter__(self)->Iterator[int]:...defcollect(items:Iterable[int])->int:...result:int=collect(Bucket())# Passes type check

Note that ABCs intyping module already provide structural behaviorat runtime,isinstance(Bucket(),Iterable) returnsTrue.The main goal of this proposal is to support such behavior statically.The same functionality will be provided for user-defined protocols, asspecified below. The above code with a protocol class matches common Pythonconventions much better. It is also automatically extensible and workswith additional, unrelated classes that happen to implementthe required protocol.

Nominal vs structural subtyping

Structural subtyping is natural for Python programmers since it matchesthe runtime semantics of duck typing: an object that has certain propertiesis treated independently of its actual runtime class.However, as discussed inPEP 483, both nominal and structuralsubtyping have their strengths and weaknesses. Therefore, in this PEP wedo not propose to replace the nominal subtyping described byPEP 484 withstructural subtyping completely. Instead, protocol classes as specified inthis PEP complement normal classes, and users are free to choosewhere to apply a particular solution. See section onrejected ideas at the end of this PEP for additional motivation.

Non-goals

At runtime, protocol classes will be simple ABCs. There is no intent toprovide sophisticated runtime instance and class checks against protocolclasses. This would be difficult and error-prone and will contradict the logicofPEP 484. As well, followingPEP 484 andPEP 526 we state that protocols arecompletely optional:

  • No runtime semantics will be imposed for variables or parameters annotatedwith a protocol class.
  • Any checks will be performed only by third-party type checkers andother tools.
  • Programmers are free to not use them even if they use type annotations.
  • There is no intent to make protocols non-optional in the future.

To reiterate, providing complex runtime semantics for protocol classesis not a goal of this PEP, the main goal is to provide a support and standardsforstatic structural subtyping. The possibility to use protocolsin the runtime context as ABCs is rather a minor bonus that exists mostlyto provide a seamless transition for projects that already use ABCs.

Existing Approaches to Structural Subtyping

Before describing the actual specification, we review and comment on existingapproaches related to structural subtyping in Python and other languages:

  • zope.interface[zope-interfaces] was one of the first widely usedapproaches to structural subtyping in Python. It is implemented by providingspecial classes to distinguish interface classes from normal classes,to mark interface attributes, and to explicitly declare implementation.For example:
    fromzope.interfaceimportInterface,Attribute,implementerclassIEmployee(Interface):name=Attribute("Name of employee")defdo(work):"""Do some work"""@implementer(IEmployee)classEmployee:name='Anonymous'defdo(self,work):returnwork.start()

    Zope interfaces support various contracts and constraints for interfaceclasses. For example:

    fromzope.interfaceimportinvariantdefrequired_contact(obj):ifnot(obj.emailorobj.phone):raiseException("At least one contact info is required")classIPerson(Interface):name=Attribute("Name")email=Attribute("Email Address")phone=Attribute("Phone Number")invariant(required_contact)

    Even more detailed invariants are supported. However, Zope interfaces relyentirely on runtime validation. Such focus on runtime properties goesbeyond the scope of the current proposal, and static support for invariantsmight be difficult to implement. However, the idea of marking an interfaceclass with a special base class is reasonable and easy to implement bothstatically and at runtime.

  • Python abstract base classes[abstract-classes] are the standardlibrary tool to provide some functionality similar to structural subtyping.The drawback of this approach is the necessity to either subclassthe abstract class or register an implementation explicitly:
    fromabcimportABCclassMyTuple(ABC):passMyTuple.register(tuple)assertissubclass(tuple,MyTuple)assertisinstance((),MyTuple)

    As mentioned in therationale,we want to avoid such necessity, especially in static context.However, in a runtime context, ABCs are good candidates forprotocol classes and they are already used extensively inthetyping module.

  • Abstract classes defined incollections.abc module[collections-abc]are slightly more advanced since they implement a custom__subclasshook__() method that allows runtime structural checks withoutexplicit registration:
    fromcollections.abcimportIterableclassMyIterable:def__iter__(self):return[]assertisinstance(MyIterable(),Iterable)

    Such behavior seems to be a perfect fit for both runtime and static behaviorof protocols. As discussed inrationale,we propose to add static support for such behavior.In addition, to allow users to achieve such runtimebehavior foruser-defined protocols a special@runtime_checkable decoratorwill be provided, see detaileddiscussion below.

  • TypeScript[typescript] provides support for user-defined classes andinterfaces. Explicit implementation declaration is not required andstructural subtyping is verified statically. For example:
    interface LabeledItem {    label: string;    size?: int;}function printLabel(obj: LabeledItem) {    console.log(obj.label);}let myObj = {size: 10, label: "Size 10 Object"};printLabel(myObj);

    Note that optional interface members are supported. Also, TypeScriptprohibits redundant members in implementations. While the idea ofoptional members looks interesting, it would complicate this proposal andit is not clear how useful it will be. Therefore, it is proposed to postponethis; seerejected ideas. In general, the idea ofstatic protocol checking without runtime implications looks reasonable,and basically this proposal follows the same line.

  • Go[golang] uses a more radical approach and makes interfaces the primaryway to provide type information. Also, assignments are used to explicitlyensure implementation:
    typeSomeInterfaceinterface{SomeMethod()([]byte,error)}if_,ok:=someval.(SomeInterface);ok{fmt.Printf("value implements some interface")}

    Both these ideas are questionable in the context of this proposal. Seethe section onrejected ideas.

Specification

Terminology

We propose to use the termprotocols for types supporting structuralsubtyping. The reason is that the termiterator protocol,for example, is widely understood in the community, and coming up witha new term for this concept in a statically typed context would just createconfusion.

This has the drawback that the termprotocol becomes overloaded withtwo subtly different meanings: the first is the traditional, well-known butslightly fuzzy concept of protocols such as iterator; the second is the moreexplicitly defined concept of protocols in statically typed code.The distinction is not important most of the time, and in othercases we propose to just add a qualifier such asprotocol classeswhen referring to the static type concept.

If a class includes a protocol in its MRO, the class is calledanexplicit subclass of the protocol. If a class is a structural subtypeof a protocol, it is said to implement the protocol and to be compatiblewith a protocol. If a class is compatible with a protocol but the protocolis not included in the MRO, the class is animplicit subtypeof the protocol. (Note that one can explicitly subclass a protocol andstill not implement it if a protocol attribute is set toNonein the subclass, see Python[data-model] for details.)

The attributes (variables and methods) of a protocol that are mandatoryfor other class in order to be considered a structural subtype are calledprotocol members.

Defining a protocol

Protocols are defined by including a special new classtyping.Protocol(an instance ofabc.ABCMeta) in the base classes list, typicallyat the end of the list. Here is a simple example:

fromtypingimportProtocolclassSupportsClose(Protocol):defclose(self)->None:...

Now if one defines a classResource with aclose() method that hasa compatible signature, it would implicitly be a subtype ofSupportsClose, since the structural subtyping is used forprotocol types:

classResource:...defclose(self)->None:self.file.close()self.lock.release()

Apart from few restrictions explicitly mentioned below, protocol types canbe used in every context where a normal types can:

defclose_all(things:Iterable[SupportsClose])->None:fortinthings:t.close()f=open('foo.txt')r=Resource()close_all([f,r])# OK!close_all([1])# Error: 'int' has no 'close' method

Note that both the user-defined classResource and the built-inIO type (the return type ofopen()) are considered subtypes ofSupportsClose, because they provide aclose() method witha compatible type signature.

Protocol members

All methods defined in the protocol class body are protocol members, bothnormal and decorated with@abstractmethod. If any parameters of aprotocol method are not annotated, then their types are assumed to beAny(seePEP 484). Bodies of protocol methods are type checked.An abstract method that should not be called viasuper() ought to raiseNotImplementedError. Example:

fromtypingimportProtocolfromabcimportabstractmethodclassExample(Protocol):deffirst(self)->int:# This is a protocol memberreturn42@abstractmethoddefsecond(self)->int:# Method without a default implementationraiseNotImplementedError

Static methods, class methods, and properties are equally allowedin protocols.

To define a protocol variable, one can usePEP 526 variableannotations in the class body. Additional attributesonly defined inthe body of a method by assignment viaself are not allowed. The rationalefor this is that the protocol class implementation is often not shared bysubtypes, so the interface should not depend on the default implementation.Examples:

fromtypingimportProtocol,ListclassTemplate(Protocol):name:str# This is a protocol membervalue:int=0# This one too (with default)defmethod(self)->None:self.temp:List[int]=[]# Error in type checkerclassConcrete:def__init__(self,name:str,value:int)->None:self.name=nameself.value=valuedefmethod(self)->None:returnvar:Template=Concrete('value',42)# OK

To distinguish between protocol class variables and protocol instancevariables, the specialClassVar annotation should be used as specifiedbyPEP 526. By default, protocol variables as defined above are consideredreadable and writable. To define a read-only protocol variable, one can usean (abstract) property.

Explicitly declaring implementation

To explicitly declare that a certain class implements a given protocol,it can be used as a regular base class. In this case a class could usedefault implementations of protocol members. Static analysis tools areexpected to automatically detect that a class implements a given protocol.So while it’s possible to subclass a protocol explicitly, it’snot necessaryto do so for the sake of type-checking.

The default implementations cannot be used ifthe subtype relationship is implicit and only via structuralsubtyping – the semantics of inheritance is not changed. Examples:

classPColor(Protocol):@abstractmethoddefdraw(self)->str:...defcomplex_method(self)->int:# some complex code here...classNiceColor(PColor):defdraw(self)->str:return"deep blue"classBadColor(PColor):defdraw(self)->str:returnsuper().draw()# Error, no default implementationclassImplicitColor:# Note no 'PColor' base heredefdraw(self)->str:return"probably gray"defcomplex_method(self)->int:# class needs to implement this...nice:NiceColoranother:ImplicitColordefrepresent(c:PColor)->None:print(c.draw(),c.complex_method())represent(nice)# OKrepresent(another)# Also OK

Note that there is little difference between explicit and implicitsubtypes, the main benefit of explicit subclassing is to get some protocolmethods “for free”. In addition, type checkers can statically verify thatthe class actually implements the protocol correctly:

classRGB(Protocol):rgb:Tuple[int,int,int]@abstractmethoddefintensity(self)->int:return0classPoint(RGB):def__init__(self,red:int,green:int,blue:str)->None:self.rgb=red,green,blue# Error, 'blue' must be 'int'# Type checker might warn that 'intensity' is not defined

A class can explicitly inherit from multiple protocols and also from normalclasses. In this case methods are resolved using normal MRO and a type checkerverifies that all subtyping are correct. The semantics of@abstractmethodis not changed, all of them must be implemented by an explicit subclassbefore it can be instantiated.

Merging and extending protocols

The general philosophy is that protocols are mostly like regular ABCs,but a static type checker will handle them specially. Subclassing a protocolclass would not turn the subclass into a protocol unless it also hastyping.Protocol as an explicit base class. Without this base, the classis “downgraded” to a regular ABC that cannot be used with structuralsubtyping. The rationale for this rule is that we don’t want to accidentallyhave some class act as a protocol just because one of its base classeshappens to be one. We still slightly prefer nominal subtyping over structuralsubtyping in the static typing world.

A subprotocol can be defined by havingboth one or more protocols asimmediate base classes and also havingtyping.Protocol as an immediatebase class:

fromtypingimportSized,ProtocolclassSizedAndClosable(Sized,Protocol):defclose(self)->None:...

Now the protocolSizedAndClosable is a protocol with two methods,__len__ andclose. If one omitsProtocol in the base class list,this would be a regular (non-protocol) class that must implementSized.Alternatively, one can implementSizedAndClosable protocol by mergingtheSupportsClose protocol from the example in thedefinition sectionwithtyping.Sized:

fromtypingimportSizedclassSupportsClose(Protocol):defclose(self)->None:...classSizedAndClosable(Sized,SupportsClose,Protocol):pass

The two definitions ofSizedAndClosable are equivalent.Subclass relationships between protocols are not meaningful whenconsidering subtyping, since structural compatibility isthe criterion, not the MRO.

IfProtocol is included in the base class list, all the other base classesmust be protocols. A protocol can’t extend a regular class, seerejected ideas for rationale.Note that rules around explicit subclassing are differentfrom regular ABCs, where abstractness is simply defined by having at least oneabstract method being unimplemented. Protocol classes must be markedexplicitly.

Generic protocols

Generic protocols are important. For example,SupportsAbs,IterableandIterator are generic protocols. They are defined similar to normalnon-protocol generic types:

classIterable(Protocol[T]):@abstractmethoddef__iter__(self)->Iterator[T]:...

Protocol[T,S,...] is allowed as a shorthand forProtocol,Generic[T,S,...].

User-defined generic protocols support explicitly declared variance.Type checkers will warn if the inferred variance is different fromthe declared variance. Examples:

T=TypeVar('T')T_co=TypeVar('T_co',covariant=True)T_contra=TypeVar('T_contra',contravariant=True)classBox(Protocol[T_co]):defcontent(self)->T_co:...box:Box[float]second_box:Box[int]box=second_box# This is OK due to the covariance of 'Box'.classSender(Protocol[T_contra]):defsend(self,data:T_contra)->int:...sender:Sender[float]new_sender:Sender[int]new_sender=sender# OK, 'Sender' is contravariant.classProto(Protocol[T]):attr:T# this class is invariant, since it has a mutable attributevar:Proto[float]another_var:Proto[int]var=another_var# Error! 'Proto[float]' is incompatible with 'Proto[int]'.

Note that unlike nominal classes, de facto covariant protocols cannot bedeclared as invariant, since this can break transitivity of subtyping(seerejected ideas for details). For example:

T=TypeVar('T')classAnotherBox(Protocol[T]):# Error, this protocol is covariant in T,defcontent(self)->T:# not invariant....

Recursive protocols

Recursive protocols are also supported. Forward references to the protocolclass names can be given as strings as specified byPEP 484. Recursiveprotocols are useful for representing self-referential data structureslike trees in an abstract fashion:

classTraversable(Protocol):defleaves(self)->Iterable['Traversable']:...

Note that for recursive protocols, a class is considered a subtype ofthe protocol in situations where the decision depends on itself.Continuing the previous example:

classSimpleTree:defleaves(self)->List['SimpleTree']:...root:Traversable=SimpleTree()# OKclassTree(Generic[T]):defleaves(self)->List['Tree[T]']:...defwalk(graph:Traversable)->None:...tree:Tree[float]=Tree()walk(tree)# OK, 'Tree[float]' is a subtype of 'Traversable'

Self-types in protocols

The self-types in protocols follow thecorresponding specificationofPEP 484. For example:

C=TypeVar('C',bound='Copyable')classCopyable(Protocol):defcopy(self:C)->C:classOne:defcopy(self)->'One':...T=TypeVar('T',bound='Other')classOther:defcopy(self:T)->T:...c:Copyablec=One()# OKc=Other()# Also OK

Callback protocols

Protocols can be used to define flexible callback types that are hard(or even impossible) to express using theCallable[...] syntaxspecified byPEP 484, such as variadic, overloaded, and complex genericcallbacks. They can be defined as protocols with a__call__ member:

fromtypingimportOptional,List,ProtocolclassCombiner(Protocol):def__call__(self,*vals:bytes,maxlen:Optional[int]=None)->List[bytes]:...defgood_cb(*vals:bytes,maxlen:Optional[int]=None)->List[bytes]:...defbad_cb(*vals:bytes,maxitems:Optional[int])->List[bytes]:...comb:Combiner=good_cb# OKcomb=bad_cb# Error! Argument 2 has incompatible type because of# different name and kind in the callback

Callback protocols andCallable[...] types can be used interchangeably.

Using Protocols

Subtyping relationships with other types

Protocols cannot be instantiated, so there are no values whoseruntime type is a protocol. For variables and parameters with protocol types,subtyping relationships are subject to the following rules:

  • A protocol is never a subtype of a concrete type.
  • A concrete typeX is a subtype of protocolPif and only ifX implements all protocol members ofP withcompatible types. In other words, subtyping with respect to a protocol isalways structural.
  • A protocolP1 is a subtype of another protocolP2 ifP1 definesall protocol members ofP2 with compatible types.

Generic protocol types follow the same rules of variance as non-protocoltypes. Protocol types can be used in all contexts where any other typescan be used, such as inUnion,ClassVar, type variables bounds, etc.Generic protocols follow the rules for generic abstract classes, except forusing structural compatibility instead of compatibility defined byinheritance relationships.

Static type checkers will recognize protocol implementations, even if thecorresponding protocols arenot imported:

# file lib.pyfromtypingimportSizedT=TypeVar('T',contravariant=True)classListLike(Sized,Protocol[T]):defappend(self,x:T)->None:passdefpopulate(lst:ListLike[int])->None:...# file main.pyfromlibimportpopulate# Note that ListLike is NOT importedclassMockStack:def__len__(self)->int:return42defappend(self,x:int)->None:print(x)populate([1,2,3])# Passes type checkpopulate(MockStack())# Also OK

Unions and intersections of protocols

Union of protocol classes behaves the same way as for non-protocolclasses. For example:

fromtypingimportUnion,Optional,ProtocolclassExitable(Protocol):defexit(self)->int:...classQuittable(Protocol):defquit(self)->Optional[int]:...deffinish(task:Union[Exitable,Quittable])->int:...classDefaultJob:...defquit(self)->int:return0finish(DefaultJob())# OK

One can use multiple inheritance to define an intersection of protocols.Example:

fromtypingimportIterable,HashableclassHashableFloats(Iterable[float],Hashable,Protocol):passdefcached_func(args:HashableFloats)->float:...cached_func((1,2,3))# OK, tuple is both hashable and iterable

If this will prove to be a widely used scenario, then a specialintersection type construct could be added in future as specified byPEP 483,seerejected ideas for more details.

Type[] and class objects vs protocols

Variables and parameters annotated withType[Proto] accept only concrete(non-protocol) subtypes ofProto. The main reason for this is to allowinstantiation of parameters with such type. For example:

classProto(Protocol):@abstractmethoddefmeth(self)->int:...classConcrete:defmeth(self)->int:return42deffun(cls:Type[Proto])->int:returncls().meth()# OKfun(Proto)# Errorfun(Concrete)# OK

The same rule applies to variables:

var:Type[Proto]var=Proto# Errorvar=Concrete# OKvar().meth()# OK

Assigning an ABC or a protocol class to a variable is allowed if it isnot explicitly typed, and such assignment creates a type alias.For normal (non-abstract) classes, the behavior ofType[] isnot changed.

A class object is considered an implementation of a protocol if accessingall members on it results in types compatible with the protocol members.For example:

fromtypingimportAny,ProtocolclassProtoA(Protocol):defmeth(self,x:int)->int:...classProtoB(Protocol):defmeth(self,obj:Any,x:int)->int:...classC:defmeth(self,x:int)->int:...a:ProtoA=C# Type check error, signatures don't match!b:ProtoB=C# OK

NewType() and type aliases

Protocols are essentially anonymous. To emphasize this point, static typecheckers might refuse protocol classes insideNewType() to avoid anillusion that a distinct type is provided:

fromtypingimportNewType,Protocol,IteratorclassId(Protocol):code:intsecrets:Iterator[bytes]UserId=NewType('UserId',Id)# Error, can't provide distinct type

In contrast, type aliases are fully supported, including generic typealiases:

fromtypingimportTypeVar,Reversible,Iterable,SizedT=TypeVar('T')classSizedIterable(Iterable[T],Sized,Protocol):passCompatReversible=Union[Reversible[T],SizedIterable[T]]

Modules as implementations of protocols

A module object is accepted where a protocol is expected if the publicinterface of the given module is compatible with the expected protocol.For example:

# file default_config.pytimeout=100one_flag=Trueother_flag=False# file main.pyimportdefault_configfromtypingimportProtocolclassOptions(Protocol):timeout:intone_flag:boolother_flag:booldefsetup(options:Options)->None:...setup(default_config)# OK

To determine compatibility of module level functions, theself argumentof the corresponding protocol methods is dropped. For example:

# callbacks.pydefon_error(x:int)->None:...defon_success()->None:...# main.pyimportcallbacksfromtypingimportProtocolclassReporter(Protocol):defon_error(self,x:int)->None:...defon_success(self)->None:...rp:Reporter=callbacks# Passes type check

@runtime_checkable decorator and narrowing types byisinstance()

The default semantics is thatisinstance() andissubclass() failfor protocol types. This is in the spirit of duck typing – protocolsbasically would be used to model duck typing statically, not explicitlyat runtime.

However, it should be possible for protocol types to implement custominstance and class checks when this makes sense, similar to howIterableand other ABCs incollections.abc andtyping already do it,but this is limited to non-generic and unsubscripted generic protocols(Iterable is statically equivalent toIterable[Any]).Thetyping module will define a special@runtime_checkable class decoratorthat provides the same semantics for class and instance checks as forcollections.abc classes, essentially making them “runtime protocols”:

fromtypingimportruntime_checkable,Protocol@runtime_checkableclassSupportsClose(Protocol):defclose(self):...assertisinstance(open('some/file'),SupportsClose)

Note that instance checks are not 100% reliable statically, this is whythis behavior is opt-in, see section onrejectedideas for examples.The most type checkers can do is to treatisinstance(obj,Iterator)roughly as a simpler way to writehasattr(x,'__iter__')andhasattr(x,'__next__'). To minimizethe risks for this feature, the following rules are applied.

Definitions:

  • Data, and non-data protocols: A protocol is called non-data protocolif it only contains methods as members (for exampleSized,Iterator, etc). A protocol that contains at least one non-method member(likex:int) is called a data protocol.
  • Unsafe overlap: A typeX is called unsafely overlapping witha protocolP, ifX is not a subtype ofP, but it is a subtypeof the type erased version ofP where all members have typeAny.In addition, if at least one element of a union unsafely overlaps witha protocolP, then the whole union is unsafely overlapping withP.

Specification:

  • A protocol can be used as a second argument inisinstance() andissubclass() only if it is explicitly opt-in by@runtime_checkabledecorator. This requirement exists because protocol checks are not type safein case of dynamically set attributes, and because type checkers can only provethat anisinstance() check is safe only for a given class, not for all itssubclasses.
  • isinstance() can be used with both data and non-data protocols, whileissubclass() can be used only with non-data protocols. This restrictionexists because some data attributes can be set on an instance in constructorand this information is not always available on the class object.
  • Type checkers should reject anisinstance() orissubclass() call, ifthere is an unsafe overlap between the type of the first argument andthe protocol.
  • Type checkers should be able to select a correct element from a union aftera safeisinstance() orissubclass() call. For narrowing from non-uniontypes, type checkers can use their best judgement (this is intentionallyunspecified, since a precise specification would require intersection types).

Using Protocols in Python 2.7 - 3.5

Variable annotation syntax was added in Python 3.6, so that the syntaxfor defining protocol variables proposed inspecification section can’tbe used if support for earlier versions is needed. To define thesein a manner compatible with older versions of Python one can use properties.Properties can be settable and/or abstract if needed:

classFoo(Protocol):@propertydefc(self)->int:return42# Default value can be provided for property...@abstractpropertydefd(self)->int:# ... or it can be abstractreturn0

Also function type comments can be used as perPEP 484 (for exampleto provide compatibility with Python 2). Thetyping module changesproposed in this PEP will also be backported to earlier versions via thebackport currently available on PyPI.

Runtime Implementation of Protocol Classes

Implementation details

The runtime implementation could be done in pure Python without anyeffects on the core interpreter and standard library except in thetyping module, and a minor update tocollections.abc:

  • Define classtyping.Protocol similar totyping.Generic.
  • Implement functionality to detect whether a class isa protocol or not. Add a class attribute_is_protocol=Trueif that is the case. Verify that a protocol class only has protocolbase classes in the MRO (except for object).
  • Implement@runtime_checkable that allows__subclasshook__()performing structural instance and subclass checks as incollections.abcclasses.
  • All structural subtyping checks will be performed by static type checkers,such asmypy[mypy]. No additional support for protocol validation willbe provided at runtime.

Changes in the typing module

The following classes intyping module will be protocols:

  • Callable
  • Awaitable
  • Iterable,Iterator
  • AsyncIterable,AsyncIterator
  • Hashable
  • Sized
  • Container
  • Collection
  • Reversible
  • ContextManager,AsyncContextManager
  • SupportsAbs (and otherSupports* classes)

Most of these classes are small and conceptually simple. It is easy to seewhat are the methods these protocols implement, and immediately recognizethe corresponding runtime protocol counterpart.Practically, few changes will be needed intyping since some of theseclasses already behave the necessary way at runtime. Most of these will needto be updated only in the correspondingtypeshed stubs[typeshed].

All other concrete generic classes such asList,Set,IO,Deque, etc are sufficiently complex that it makes sense to keepthem non-protocols (i.e. require code to be explicit about them). Also, it istoo easy to leave some methods unimplemented by accident, and explicitlymarking the subclass relationship allows type checkers to pinpoint the missingimplementations.

Introspection

The existing class introspection machinery (dir,__annotations__ etc)can be used with protocols. In addition, all introspection tools implementedin thetyping module will support protocols. Since all attributes needto be defined in the class body based on this proposal, protocol classes willhave even better perspective for introspection than regular classes whereattributes can be defined implicitly – protocol attributes can’t beinitialized in ways that are not visible to introspection(usingsetattr(), assignment viaself, etc.). Still, some things liketypes of attributes will not be visible at runtime in Python 3.5 and earlier,but this looks like a reasonable limitation.

There will be only limited support ofisinstance() andissubclass()as discussed above (these willalways fail withTypeError forsubscripted generic protocols, since a reliable answer could not be givenat runtime in this case). But together with other introspection tools thisgive a reasonable perspective for runtime type checking tools.

Rejected/Postponed Ideas

The ideas in this section were previously discussed in[several][discussions][elsewhere].

Make every class a protocol by default

Some languages such as Go make structural subtyping the only or the primaryform of subtyping. We could achieve a similar result by making all classesprotocols by default (or even always). However we believe that it is betterto require classes to be explicitly marked as protocols, for the followingreasons:

  • Protocols don’t have some properties of regular classes. In particular,isinstance(), as defined for normal classes, is based on the nominalhierarchy. In order to make everything a protocol by default, and haveisinstance() work would require changing its semantics,which won’t happen.
  • Protocol classes should generally not have many method implementations,as they describe an interface, not an implementation.Most classes have many method implementations, making them bad protocolclasses.
  • Experience suggests that many classes are not practical as protocols anyway,mainly because their interfaces are too large, complex orimplementation-oriented (for example, they may include de factoprivate attributes and methods without a__ prefix).
  • Most actually useful protocols in existing Python code seem to be implicit.The ABCs intyping andcollections.abc are rather an exception, buteven they are recent additions to Python and most programmersdo not use them yet.
  • Many built-in functions only accept concrete instances ofint(and subclass instances), and similarly for other built-in classes. Makingint a structural type wouldn’t be safe without major changes to thePython runtime, which won’t happen.

Protocols subclassing normal classes

The main rationale to prohibit this is to preserve transitivity of subtyping,consider this example:

fromtypingimportProtocolclassBase:attr:strclassProto(Base,Protocol):defmeth(self)->int:...classC:attr:strdefmeth(self)->int:return0

Now,C is a subtype ofProto, andProto is a subtype ofBase.ButC cannot be a subtype ofBase (since the latter is nota protocol). This situation would be really weird. In addition, there isan ambiguity about whether attributes ofBase should become protocolmembers ofProto.

Support optional protocol members

We can come up with examples where it would be handy to be able to saythat a method or data attribute does not need to be present in a classimplementing a protocol, but if it is present, it must conform to a specificsignature or type. One could use ahasattr() check to determine whetherthey can use the attribute on a particular instance.

Languages such as TypeScript have similar features andapparently they are pretty commonly used. The current realistic potentialuse cases for protocols in Python don’t require these. In the interestof simplicity, we propose to not support optional methods or attributes.We can always revisit this later if there is an actual need.

Allow only protocol methods and force use of getters and setters

One could argue that protocols typically only define methods, but notvariables. However, using getters and setters in cases where only asimple variable is needed would be quite unpythonic. Moreover, the widespreaduse of properties (that often act as type validators) in large code basesis partially due to previous absence of static type checkers for Python,the problem thatPEP 484 and this PEP are aiming to solve. For example:

# without static typesclassMyClass:@propertydefmy_attr(self):returnself._my_attr@my_attr.setterdefmy_attr(self,value):ifnotisinstance(value,int):raiseValidationError("An integer expected for my_attr")self._my_attr=value# with static typesclassMyClass:my_attr:int

Support non-protocol members

There was an idea to make some methods “non-protocol” (i.e. not necessaryto implement, and inherited in explicit subclassing), but it was rejected,since this complicates things. For example, consider this situation:

classProto(Protocol):@abstractmethoddeffirst(self)->int:raiseNotImplementedErrordefsecond(self)->int:returnself.first()+1deffun(arg:Proto)->None:arg.second()

The question is should this be an error? We think most people would expectthis to be valid. Therefore, to be on the safe side, we need to require bothmethods to be implemented in implicit subclasses. In addition, if one looksat definitions incollections.abc, there are very few methods that couldbe considered “non-protocol”. Therefore, it was decided to not introduce“non-protocol” methods.

There is only one downside to this: it will require some boilerplate forimplicit subtypes of “large” protocols. But, this doesn’t apply to “built-in”protocols that are all “small” (i.e. have only few abstract methods).Also, such style is discouraged for user-defined protocols. It is recommendedto create compact protocols and combine them.

Make protocols interoperable with other approaches

The protocols as described here are basically a minimal extension tothe existing concept of ABCs. We argue that this is the way they shouldbe understood, instead of as something thatreplaces Zope interfaces,for example. Attempting such interoperabilities will significantlycomplicate both the concept and the implementation.

On the other hand, Zope interfaces are conceptually a superset of protocolsdefined here, but using an incompatible syntax to define them,because beforePEP 526 there was no straightforward way to annotate attributes.In the 3.6+ world,zope.interface might potentially adopt theProtocolsyntax. In this case, type checkers could be taught to recognize interfacesas protocols and make simple structural checks with respect to them.

Use assignments to check explicitly that a class implements a protocol

In the Go language the explicit checks for implementation are performedvia dummy assignments[golang]. Such a way is also possible with thecurrent proposal. Example:

classA:def__len__(self)->float:return..._:Sized=A()# Error: A.__len__ doesn't conform to 'Sized'# (Incompatible return type 'float')

This approach moves the check away fromthe class definition and it almost requires a comment as otherwisethe code probably would not make any sense to an average reader– it looks like dead code. Besides, in the simplest form it requires oneto construct an instance ofA, which could be problematic if this requiresaccessing or allocating some resources such as files or sockets.We could work around the latter by using a cast, for example, but thenthe code would be ugly. Therefore, we discourage the use of this pattern.

Supportisinstance() checks by default

The problem with this is instance checks could be unreliable, except forsituations where there is a common signature convention such asIterable.For example:

classP(Protocol):defcommon_method_name(self,x:int)->int:...classX:<abunchofmethods>defcommon_method_name(self)->None:...# Note different signaturedefdo_stuff(o:Union[P,X])->int:ifisinstance(o,P):returno.common_method_name(1)# Results in TypeError not caught# statically if o is an X instance.

Another potentially problematic case is assignment of attributesafter instantiation:

classP(Protocol):x:intclassC:definitialize(self)->None:self.x=0c=C()isinstance(c,P)# Falsec.initialize()isinstance(c,P)# Truedeff(x:Union[P,int])->None:ifisinstance(x,P):# Static type of x is P here....else:# Static type of x is int, but can be other type at runtime...print(x+1)f(C())# ...causing a TypeError.

We argue that requiring an explicit class decorator would be better, sinceone can then attach warnings about problems like this in the documentation.The user would be able to evaluate whether the benefits outweighthe potential for confusion for each protocol and explicitly opt in – butthe default behavior would be safer. Finally, it will be easy to make thisbehavior default if necessary, while it might be problematic to make it opt-inafter being default.

Provide a special intersection type construct

There was an idea to allowProto=All[Proto1,Proto2,...] as a shorthandfor:

classProto(Proto1,Proto2,...,Protocol):pass

However, it is not yet clear how popular/useful it will be and implementingthis in type checkers for non-protocol classes could be difficult. Finally, itwill be very easy to add this later if needed.

Prohibit explicit subclassing of protocols by non-protocols

This was rejected for the following reasons:

  • Backward compatibility: People are already using ABCs, including genericABCs fromtyping module. If we prohibit explicit subclassing of theseABCs, then quite a lot of code will break.
  • Convenience: There are existing protocol-like ABCs (that may be turnedinto protocols) that have many useful “mix-in” (non-abstract) methods.For example, in the case ofSequence one only needs to implement__getitem__ and__len__ in an explicit subclass, and one gets__iter__,__contains__,__reversed__,index, andcountfor free.
  • Explicit subclassing makes it explicit that a class implements a particularprotocol, making subtyping relationships easier to see.
  • Type checkers can warn about missing protocol members or members withincompatible types more easily, without having to use hacks like dummyassignments discussed above in this section.
  • Explicit subclassing makes it possible to force a class to be considereda subtype of a protocol (by using#type:ignore together with anexplicit base class) when it is not strictly compatible, such as whenit has an unsafe override.

Covariant subtyping of mutable attributes

Rejected because covariant subtyping of mutable attributes is not safe.Consider this example:

classP(Protocol):x:floatdeff(arg:P)->None:arg.x=0.42classC:x:intc=C()f(c)# Would typecheck if covariant subtyping# of mutable attributes were allowed.c.x>>1# But this fails at runtime

It was initially proposed to allow this for practical reasons, but it wassubsequently rejected, since this may mask some hard to spot bugs.

Overriding inferred variance of protocol classes

It was proposed to allow declaring protocols as invariant if they are actuallycovariant or contravariant (as it is possible for nominal classes, seePEP 484).However, it was decided not to do this because of several downsides:

  • Declared protocol invariance breaks transitivity of sub-typing. Considerthis situation:
    T=TypeVar('T')classP(Protocol[T]):# Protocol is declared as invariant.defmeth(self)->T:...classC:defmeth(self)->float:...classD(C):defmeth(self)->int:...

    Now we have thatD is a subtype ofC, andC is a subtype ofP[float]. ButD isnot a subtype ofP[float] sinceDimplementsP[int], andP is invariant. There is a possibilityto “cure” this by looking for protocol implementations in MROs but thiswill be too complex in a general case, and this “cure” requires abandoningsimple idea of purely structural subtyping for protocols.

  • Subtyping checks will always require type inference for protocols. In theabove example a user may complain: “Why did you inferP[int] formyD? It implementsP[float]!”. Normally, inference can be overruledby an explicit annotation, but here this will require explicit subclassing,defeating the purpose of using protocols.
  • Allowing overriding variance will make impossible more detailed errormessages in type checkers citing particular conflicts in membertype signatures.
  • Finally, explicit is better than implicit in this case. Requiring user todeclare correct variance will simplify understanding the code and will avoidunexpected errors at the point of use.

Support adapters and adaptation

Adaptation was proposed byPEP 246 (rejected) and is supported byzope.interface, seethe Zope documentation on adapter registries.Adapters is quite an advanced concept, andPEP 484 supports unions andgeneric aliases that can be used instead of adapters. This can be illustratedwith an example ofIterable protocol, there is another way of supportingiteration by providing__getitem__ and__len__. If a functionsupports both this way and the now standard__iter__ method, then it couldbe annotated by a union type:

classOldIterable(Sized,Protocol[T]):def__getitem__(self,item:int)->T:...CompatIterable=Union[Iterable[T],OldIterable[T]]classA:def__iter__(self)->Iterator[str]:...classB:def__len__(self)->int:...def__getitem__(self,item:int)->str:...defiterate(it:CompatIterable[str])->None:...iterate(A())# OKiterate(B())# OK

Since there is a reasonable alternative for such cases with existing tooling,it is therefore proposed not to include adaptation in this PEP.

Call structural base types “interfaces”

“Protocol” is a term already widely used in Python to describe duck typingcontracts such as the iterator protocol (providing__iter__and__next__), and the descriptor protocol (providing__get__,__set__, and__delete__). In addition to this and other reasons giveninspecification, protocols are different from Java interfaces in severalaspects: protocols don’t require explicit declaration of implementation(they are mainly oriented on duck-typing), protocols can havedefault implementations of members and store state.

Make protocols special objects at runtime rather than normal ABCs

Making protocols non-ABCs will make the backwards compatibility problematicif possible at all. For example,collections.abc.Iterable is alreadyan ABC, and lots of existing code use patterns likeisinstance(obj,collections.abc.Iterable) and similar checks with otherABCs (also in a structural manner, i.e., via__subclasshook__).Disabling this behavior will cause breakages. If we keep this behaviorfor ABCs incollections.abc but will not provide a similar runtimebehavior for protocols intyping, then a smooth transition to protocolswill be not possible. In addition, having two parallel hierarchies may causeconfusions.

Backwards Compatibility

This PEP is fully backwards compatible.

Implementation

Themypy type checker fully supports protocols (modulo a fewknown bugs). This includes treating all the builtin protocols, such asIterable structurally. The runtime implementation of protocols isavailable intyping_extensions module on PyPI.

References

[typing]
https://docs.python.org/3/library/typing.html
[wiki-structural]
https://en.wikipedia.org/wiki/Structural_type_system
[zope-interfaces]
https://zopeinterface.readthedocs.io/en/latest/
[abstract-classes]
https://docs.python.org/3/library/abc.html
[collections-abc]
https://docs.python.org/3/library/collections.abc.html
[typescript]
https://www.typescriptlang.org/docs/handbook/interfaces.html
[golang] (1,2)
https://golang.org/doc/effective_go.html#interfaces_and_types
[data-model]
https://docs.python.org/3/reference/datamodel.html#special-method-names
[typeshed]
https://github.com/python/typeshed/
[mypy]
http://github.com/python/mypy/
[several]
https://mail.python.org/pipermail/python-ideas/2015-September/thread.html#35859
[discussions]
https://github.com/python/typing/issues/11
[elsewhere]
https://github.com/python/peps/pull/224

Copyright

This document has been placed in the public domain.


Source:https://github.com/python/peps/blob/main/peps/pep-0544.rst

Last modified:2025-02-01 07:28:42 GMT


[8]ページ先頭

©2009-2025 Movatter.jp