Package: optype
optypeOne protocol, one method.Building blocks for precise & flexible type hints.InstallationOptype is available asoptypeon PyPI:pipinstalloptype...ReferenceAlltyping protocolshere live in the rootoptypenamespace.
They areruntime-checkableso that you can do e.g.isinstance('snail', optype.CanAdd), in case you want to check whethersnailimplements__add__.[!NOTE]
It is bad practice to use atyping.Protocolas base class for your
implementation. Because of@typing.runtime_checkable, you can useisinstanceeither way.Unlikecollections.abc,optype's protocols aren't abstract base classes,
i.e. they don't extendabc.ABC, onlytyping.Protocol.
This allows theoptypeprotocols to be used as building blocks for.pyitype stubs.Type conversionThe return type of these special methods isinvariant. Python will raise an
error if some other (sub)type is returned.
This is why theseoptypeinterfaces don't accept generic type arguments.TypeSignatureExpressionCanBool__bool__(self) -> boolbool(self)CanInt__int__(self) -> intint(self)CanFloat__float__(self) -> floatfloat(self)CanComplex__complex__(self) -> complexcomplex(self)CanBytes__bytes__(self) -> bytesbytes(self)CanStr__str__(self) -> strstr(self)These formatting methods are allowed to return instances that are a subtype
of thestrbuiltin. The same holds for the__format__argument.
So if you're a 10x developer that wants to hack Python's f-strings, but only
if your type hints are spot-on;optypeis you friend.TypeSignatureExpressionCanRepr[Y: str]__repr__(self) -> Trepr(_)CanFormat[X: str, Y: str]__format__(self, x: X) -> Yformat(_, x)"Rich comparison" operatorsThese special methods generally abool. However, instances of any type can
be returned.TypeSignatureExpressionExpr. ReflectedCanLt[X, Y]__lt__(self, x: X) -> Yself < xx > selfCanLe[X, Y]__le__(self, x: X) -> Yself <= xx >= selfCanEq[X, Y]__eq__(self, x: X) -> Yself == xx == selfCanNe[X, Y]__ne__(self, x: X) -> Yself != xx != selfCanGt[X, Y]__gt__(self, x: X) -> Yself > xx < selfCanGe[X, Y]__ge__(self, x: X) -> Yself >= xx <= selfAttribute accessTypeSignatureExpressionCanGetattr[K: str, V]__getattr__(self, k: K) -> Vv = self.korv = getattr(self, k)CanGetattribute[K: str, V]__getattribute__(self, k: K) -> Vv = self.korv = getattr(self, k)CanSetattr[K: str, V]__setattr__(self, k: K, v: V)self.k = vorsetattr(self, k, v)CanDelattr[K: str]__delattr__(self, k: K)del self.kordelattr(self, k)CanDir[Vs: CanIter[Any]]__dir__(self) -> Vsdir(self)IterationThe operandxofiter(_)is within Python known as aniterable, which is
whatcollections.abc.Iterable[K]is often used for (e.g. as base class, or
for instance checking).Theoptypeanalogue isCanIter[Ks], which as the name suggests,
also implements__iter__. But unlikeIterable[K], its type parameterKsbinds to the return type ofiter(_). This makes it possible to annotate the
specific type of theiterablethatiter(_)returns.Iterable[K]is only
able to annotate the type of the iterated value. To see why that isn't
possible, seepython/typing#548.Thecollections.abc.Iterator[K]is even more awkward; it is a subtype ofIterable[K]. For those familiar withcollections.abcthis might come as a
surprise, but an iterator only needs to implement__next__,__iter__isn't
needed. This means that theIterator[K]is unnecessarily restrictive.
Apart from that being theoretically "ugly", it has significant performance
implications, because the time-complexity ofisinstanceon atyping.Protocolis $O(n)$, with the $n$ referring to the amount of members.
So even if the overhead of the inheritance and theabc.ABCusage is ignored,collections.abc.Iteratoris twice as slow as it needs to be.That's one of the (many) reasons thatoptype.CanNext[V]andoptype.CanNext[V]are the better alternatives toIterableandIteratorfrom the abracadabra collections. This is how they are defined:TypeSignatureExpressionCanNext[V]__next__(self) -> Vnext(self)CanIter[Vs: CanNext[Any]]__iter__(self) -> Vsiter(self)ContainersTypeSignatureExpressionCanLen__len__(self) -> intlen(self)CanLengthHint__length_hint__(self) -> intdocsCanGetitem[K, V]__getitem__(self, k: K) -> Vself[k]CanSetitem[K, V]__setitem__(self, k: K, v: V)self[k] = vCanDelitem[K]__delitem__(self, k: K)del self[k]CanMissing[K, V]__missing__(self, k: K) -> VdocsCanReversed[Y][^4]__reversed__(self) -> Yreversed(self)CanContains[K]__contains__(self, k: K) -> boolx in selfFor indexing or locating container values, the following special methods are
relevant:TypeSignatureExpressionCanHash__hash__(self) -> inthash(self)CanIndex__index__(self) -> intdocs[^4]:  Although not strictly required,Y@CanReversedshould be aCanNext.
[LH]:https://docs.python.org/3/reference/datamodel.html#object.__length_hint__
[GM]:https://docs.python.org/3/reference/datamodel.html#object.__missing__
[IX]:https://docs.python.org/3/reference/datamodel.html#object.__index__DescriptorsInterfaces fordescriptors.TypeSignatureCanGet[T: object, U, V]__get__(self, obj: None, cls: type[T]) -> U__get__(self, obj: T, cls: type[T] | None = ...) -> VCanSet[T: object, V]__set__(self, obj: T, v: V) -> AnyCanDelete[T: object]__delete__(self, obj: T) -> AnyCanSetName[T: object]__set_name__(self, cls: type[T], name: str) -> AnyCallable objectsLikecollections.abc.Callable, but without esoteric hacks.TypeSignatureExpressionCanCall[**Xs, Y]__call__(self, *xs: Xs.args, **kxs: Xs.kwargs) -> Yself(*xs, **kxs)Numeric operationsFor describing things that act like numbers. See thePython docsfor more
info.TypeSignatureExpressionCanAdd[X, Y]__add__(self, x: X) -> Yself + xCanSub[X, Y]__sub__(self, x: X) -> Yself - xCanMul[X, Y]__mul__(self, x: X) -> Yself * xCanMatmul[X, Y]__matmul__(self, x: X) -> Yself @ xCanTruediv[X, Y]__truediv__(self, x: X) -> Yself / xCanFloordiv[X, Y]__floordiv__(self, x: X) -> Yself // xCanMod[X, Y]__mod__(self, x: X) -> Yself % xCanDivmod[X, Y]__divmod__(self, x: X) -> Ydivmod(self, x)CanPow2[X, Y]__pow__(self, x: X) -> Yself ** xCanPow3[X, M, Y]__pow__(self, x: X, m: M) -> Ypow(self, x, m)CanLshift[X, Y]__lshift__(self, x: X) -> Yself << xCanRshift[X, Y]__rshift__(self, x: X) -> Yself >> xCanAnd[X, Y]__and__(self, x: X) -> Yself & xCanXor[X, Y]__xor__(self, x: X) -> Yself ^ xCanOr[X, Y]__or__(self, x: X) -> Yself | xAdditionally, there is the intersection typeCanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3], that overloads
both__pow__method signatures. Note that the2and3suffixes refer
to thearity(#parameters) of the operators.For the binary infix operators above,optypeadditionally provides
interfaces with reflected (swapped) operands:TypeSignatureExpressionCanRAdd[X, Y]__radd__(self, x: X) -> Yx + selfCanRSub[X, Y]__rsub__(self, x: X) -> Yx - selfCanRMul[X, Y]__rmul__(self, x: X) -> Yx * selfCanRMatmul[X, Y]__rmatmul__(self, x: X) -> Yx @ selfCanRTruediv[X, Y]__rtruediv__(self, x: X) -> Yx / selfCanRFloordiv[X, Y]__rfloordiv__(self, x: X) -> Yx // selfCanRMod[X, Y]__rmod__(self, x: X) -> Yx % selfCanRDivmod[X, Y]__rdivmod__(self, x: X) -> Ydivmod(x, self)CanRPow[X, Y]__rpow__(self, x: X) -> Yx ** selfCanRLshift[X, Y]__rlshift__(self, x: X) -> Yx << selfCanRRshift[X, Y]__rrshift__(self, x: X) -> Yx >> selfCanRAnd[X, Y]__rand__(self, x: X) -> Yx & selfCanRXor[X, Y]__rxor__(self, x: X) -> Yx ^ selfCanROr[X, Y]__ror__(self, x: X) -> Yx | selfNote thatCanRPowcorresponds toCanPow2; the 3-parameter "modulo"powdoes not reflect in Python.Similarly, the augmented assignment operators are described by the followingoptypeinterfaces:TypeSignatureExpressionCanIAdd[X, Y]__iadd__(self, x: X) -> Yself += xCanISub[X, Y]__isub__(self, x: X) -> Yself -= xCanIMul[X, Y]__imul__(self, x: X) -> Yself *= xCanIMatmul[X, Y]__imatmul__(self, x: X) -> Yself @= xCanITruediv[X, Y]__itruediv__(self, x: X) -> Yself /= xCanIFloordiv[X, Y]__ifloordiv__(self, x: X) -> Yself //= xCanIMod[X, Y]__imod__(self, x: X) -> Yself %= xCanIPow[X, Y]__ipow__(self, x: X) -> Yself **= xCanILshift[X, Y]__ilshift__(self, x: X) -> Yself <<= xCanIRshift[X, Y]__irshift__(self, x: X) -> Yself >>= xCanIAnd[X, Y]__iand__(self, x: X) -> Yself &= xCanIXor[X, Y]__ixor__(self, x: X) -> Yself ^= xCanIOr[X, Y]__ior__(self, x: X) -> Yself |= xAdditionally, there are the unary arithmetic operators:TypeSignatureExpressionCanPos[Y]__pos__(self) -> Y+selfCanNeg[Y]__neg__(self) -> Y-selfCanInvert[Y]__invert__(self) -> Y~selfCanAbs[Y]__abs__(self) -> Yabs(self)Theroundfunction comes in two flavors, and their overloaded intersection:TypeSignatureExpressionCanRound1[Y1]__round__(self) -> Y1round(self)CanRound2[N, Y2]__round__(self, n: N) -> Y2round(self, n)CanRound[N, Y1, Y2]__round__(self) -> Y1__round__(self, n: N) -> Y2round(self[, n: N])The last "double" signature denotes overloading.To illustrate;floatis aCanRound[int, int, float]andintaCanRound[int, int, int].And finally, the remaining rounding functions:TypeSignatureExpressionCanTrunc[Y]__trunc__(self) -> Ymath.trunc(self)CanFloor[Y]__floor__(self) -> Ymath.floor(self)CanCeil[Y]__ceil__(self) -> Ymath.ceil(self)Note that the type parameterYis unbounded, because technically these
methods can return any type.Context managersSupport for thewithstatement.TypeSignatureCanEnter[V]__enter__(self) -> VCanExit[R]__exit__(self, *exc_info: *ExcInfo) -> RIn case of errors, the type aliasExcInfowill betuple[type[E], E, types.TracebackType], whereEis someBaseException.
On the other hand, if no errors are raised (without being silenced),
thenExcinfowill beNonein triplicate.Because everyone that enters must also leave (that means you too, Barry),optypeprovides theintersection typeCanWith[V, R] = CanEnter[V] & CanExit[R].
If you're thinking of an insect-themed sect right now, that's ok --
intersection types aren't real (yet..?).
To put your mind at ease, here's how it's implemented:classCanWith[V,R](CanEnter[V],CanExit[R]):# You won't find any bugs here :)...Buffer typesInterfaces for emulating buffer types.TypeSignatureCanBuffer[B: int]__buffer__(self, flags: B) -> memoryviewCanReleaseBuffer__release_buffer__(self) -> NoneTheflags: Bparameter accepts integers within the[1, 1023]interval.
Note that theCanReleaseBufferisn't always needed.
See thePython docsorinspect.BufferFlagsfor more info.Async objectsTheoptypevariant ofcollections.abc.Awaitable[V]. The only difference
is thatoptype.CanAwait[V]is a pure interface, whereasAwaitableis
also an abstract base class.TypeSignatureExpressionCanAwait[V]__await__(self) -> Generator[Any, None, V]await selfAsync IterationYes, you guessed it right; the abracadabra collections repeated their mistakes
with their async iterablors (or something like that).But fret not, theoptypealternatives are right here:TypeSignatureExpressionCanAnext[V]__anext__(self) -> Vanext(self)CanAiter[Vs: CanAnext]__aiter__(self) -> Yaiter(self)But wait, shouldn'tVbe aCanAwait? Well, only if you don't want to get
fired...
Technically speaking,__anext__can return any type, andanextwill pass
it along without nagging (instance checks are slow, now stop bothering that
liberal).
Just because something is legal, doesn't mean it's a good idea (don't eat the
yellow snow).Async context managersSupport for theasync withstatement.TypeSignatureCanAenter[V]__aenter__(self) -> CanAwait[V]CanAexit[R]__aexit__(self, *exc_info: *ExcInfo) -> CanAwait[R]And just likeCanWith[V, R]for synccontext managers,
there is theCanAsyncWith[V, R] = CanAenter[V] & CanAexit[R]intersection
type.Future plansSupport for Python versions before 3.12.A drop-in replacement for theoperatorstandard library, with
runtime-accessible type annotations, and more operators.More standard library protocols, e.g.copy,dataclasses,pickle.Typed mixins for DRY implementation of operator, e.g. for comparison opsGeFromLt,GtFromLe, etc as a typed alternative forfunctools.total_ordering. Similarly for numeric types, with e.g.__add__and__neg__a mixin could generate__pos__and__sub__, or with__mod__and__truediv__a mixin could generate__Dependency-free third-party type support, e.g. protocols fornumpy's array
interface.