Typing Protocols for Precise Type Hints in Python 3.12+
Project description
optype
One protocol, one method.
Building blocks for precise & flexible type hints.
Installation
Optype is available as optype
on PyPI:
pip install optype
...
Reference
All typing protocols here live in the root optype
namespace.
They are runtime-checkable so that you can do e.g.
isinstance('snail', optype.CanAdd)
, in case you want to check whether
snail
implements __add__
.
[!NOTE] It is bad practice to use a
typing.Protocol
as base class for your implementation. Because of@typing.runtime_checkable
, you can useisinstance
either way.
Unlikecollections.abc
, optype
's protocols aren't abstract base classes,
i.e. they don't extend abc.ABC
, only typing.Protocol
.
This allows the optype
protocols to be used as building blocks for .pyi
type stubs.
Type conversion
The return type of these special methods is invariant. Python will raise an
error if some other (sub)type is returned.
This is why these optype
interfaces don't accept generic type arguments.
Type | Signature | Expression |
---|---|---|
CanBool |
__bool__(self) -> bool |
bool(self) |
CanInt |
__int__(self) -> int |
int(self) |
CanFloat |
__float__(self) -> float |
float(self) |
CanComplex |
__complex__(self) -> complex |
complex(self) |
CanBytes |
__bytes__(self) -> bytes |
bytes(self) |
CanStr |
__str__(self) -> str |
str(self) |
These formatting methods are allowed to return instances that are a subtype
of the str
builtin. 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; optype
is you friend.
Type | Signature | Expression |
---|---|---|
CanRepr[Y: str] |
__repr__(self) -> T |
repr(_) |
CanFormat[X: str, Y: str] |
__format__(self, x: X) -> Y |
format(_, x) |
"Rich comparison" operators
These special methods generally a bool
. However, instances of any type can
be returned.
Type | Signature | Expression | Expr. Reflected |
---|---|---|---|
CanLt[X, Y] |
__lt__(self, x: X) -> Y |
self < x |
x > self |
CanLe[X, Y] |
__le__(self, x: X) -> Y |
self <= x |
x >= self |
CanEq[X, Y] |
__eq__(self, x: X) -> Y |
self == x |
x == self |
CanNe[X, Y] |
__ne__(self, x: X) -> Y |
self != x |
x != self |
CanGt[X, Y] |
__gt__(self, x: X) -> Y |
self > x |
x < self |
CanGe[X, Y] |
__ge__(self, x: X) -> Y |
self >= x |
x <= self |
Attribute access
Type | Signature | Expression |
---|---|---|
CanGetattr[K: str, V] |
__getattr__(self, k: K) -> V |
v = self.k orv = getattr(self, k)
|
CanGetattribute[K: str, V] |
__getattribute__(self, k: K) -> V |
v = self.k or v = getattr(self, k)
|
CanSetattr[K: str, V] |
__setattr__(self, k: K, v: V) |
self.k = v orsetattr(self, k, v)
|
CanDelattr[K: str] |
__delattr__(self, k: K) |
del self.k ordelattr(self, k)
|
CanDir[Vs: CanIter[Any]] |
__dir__(self) -> Vs |
dir(self) |
Iteration
The operand x
of iter(_)
is within Python known as an iterable, which is
what collections.abc.Iterable[K]
is often used for (e.g. as base class, or
for instance checking).
The optype
analogue is CanIter[Ks]
, which as the name suggests,
also implements __iter__
. But unlike Iterable[K]
, its type parameter Ks
binds to the return type of iter(_)
. This makes it possible to annotate the
specific type of the iterable that iter(_)
returns. Iterable[K]
is only
able to annotate the type of the iterated value. To see why that isn't
possible, see python/typing#548.
The collections.abc.Iterator[K]
is even more awkward; it is a subtype of
Iterable[K]
. For those familiar with collections.abc
this might come as a
surprise, but an iterator only needs to implement __next__
, __iter__
isn't
needed. This means that the Iterator[K]
is unnecessarily restrictive.
Apart from that being theoretically "ugly", it has significant performance
implications, because the time-complexity of isinstance
on a
typing.Protocol
is $O(n)$, with the $n$ referring to the amount of members.
So even if the overhead of the inheritance and the abc.ABC
usage is ignored,
collections.abc.Iterator
is twice as slow as it needs to be.
That's one of the (many) reasons that optype.CanNext[V]
and
optype.CanNext[V]
are the better alternatives to Iterable
and Iterator
from the abracadabra collections. This is how they are defined:
Type | Signature | Expression |
---|---|---|
CanNext[V] |
__next__(self) -> V |
next(self) |
CanIter[Vs: CanNext[Any]] |
__iter__(self) -> Vs |
iter(self) |
Containers
Type | Signature | Expression |
---|---|---|
CanLen |
__len__(self) -> int |
len(self) |
CanLengthHint |
__length_hint__(self) -> int |
docs |
CanGetitem[K, V] |
__getitem__(self, k: K) -> V |
self[k] |
CanSetitem[K, V] |
__setitem__(self, k: K, v: V) |
self[k] = v |
CanDelitem[K] |
__delitem__(self, k: K) |
del self[k] |
CanMissing[K, V] |
__missing__(self, k: K) -> V |
docs |
CanReversed[Y] [^4] |
__reversed__(self) -> Y |
reversed(self) |
CanContains[K] |
__contains__(self, k: K) -> bool |
x in self |
For indexing or locating container values, the following special methods are relevant:
Type | Signature | Expression |
---|---|---|
CanHash |
__hash__(self) -> int |
hash(self) |
CanIndex |
__index__(self) -> int |
docs |
[^4]: Although not strictly required, Y@CanReversed
should be a CanNext
.
[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__
Descriptors
Interfaces for descriptors.
Type | Signature |
---|---|
CanGet[T: object, U, V] |
__get__(self, obj: None, cls: type[T]) -> U __get__(self, obj: T, cls: type[T] | None = ...) -> V
|
CanSet[T: object, V] |
__set__(self, obj: T, v: V) -> Any |
CanDelete[T: object] |
__delete__(self, obj: T) -> Any |
CanSetName[T: object] |
__set_name__(self, cls: type[T], name: str) -> Any
|
Callable objects
Like collections.abc.Callable
, but without esoteric hacks.
Type | Signature | Expression |
---|---|---|
CanCall[**Xs, Y] |
__call__(self, *xs: Xs.args, **kxs: Xs.kwargs) -> Y
|
self(*xs, **kxs) |
Numeric operations
For describing things that act like numbers. See the Python docs for more info.
Type | Signature | Expression |
---|---|---|
CanAdd[X, Y] |
__add__(self, x: X) -> Y |
self + x |
CanSub[X, Y] |
__sub__(self, x: X) -> Y |
self - x |
CanMul[X, Y] |
__mul__(self, x: X) -> Y |
self * x |
CanMatmul[X, Y] |
__matmul__(self, x: X) -> Y |
self @ x |
CanTruediv[X, Y] |
__truediv__(self, x: X) -> Y |
self / x |
CanFloordiv[X, Y] |
__floordiv__(self, x: X) -> Y |
self // x |
CanMod[X, Y] |
__mod__(self, x: X) -> Y |
self % x |
CanDivmod[X, Y] |
__divmod__(self, x: X) -> Y |
divmod(self, x) |
CanPow2[X, Y] |
__pow__(self, x: X) -> Y |
self ** x |
CanPow3[X, M, Y] |
__pow__(self, x: X, m: M) -> Y |
pow(self, x, m) |
CanLshift[X, Y] |
__lshift__(self, x: X) -> Y |
self << x |
CanRshift[X, Y] |
__rshift__(self, x: X) -> Y |
self >> x |
CanAnd[X, Y] |
__and__(self, x: X) -> Y |
self & x |
CanXor[X, Y] |
__xor__(self, x: X) -> Y |
self ^ x |
CanOr[X, Y] |
__or__(self, x: X) -> Y |
self | x |
Additionally, there is the intersection type
CanPow[X, M, Y2, Y3] =: CanPow2[X, Y2] & CanPow3[X, M, Y3]
, that overloads
both __pow__
method signatures. Note that the 2
and 3
suffixes refer
to the arity (#parameters) of the operators.
For the binary infix operators above, optype
additionally provides
interfaces with reflected (swapped) operands:
Type | Signature | Expression |
---|---|---|
CanRAdd[X, Y] |
__radd__(self, x: X) -> Y |
x + self |
CanRSub[X, Y] |
__rsub__(self, x: X) -> Y |
x - self |
CanRMul[X, Y] |
__rmul__(self, x: X) -> Y |
x * self |
CanRMatmul[X, Y] |
__rmatmul__(self, x: X) -> Y |
x @ self |
CanRTruediv[X, Y] |
__rtruediv__(self, x: X) -> Y |
x / self |
CanRFloordiv[X, Y] |
__rfloordiv__(self, x: X) -> Y |
x // self |
CanRMod[X, Y] |
__rmod__(self, x: X) -> Y |
x % self |
CanRDivmod[X, Y] |
__rdivmod__(self, x: X) -> Y |
divmod(x, self) |
CanRPow[X, Y] |
__rpow__(self, x: X) -> Y |
x ** self |
CanRLshift[X, Y] |
__rlshift__(self, x: X) -> Y |
x << self |
CanRRshift[X, Y] |
__rrshift__(self, x: X) -> Y |
x >> self |
CanRAnd[X, Y] |
__rand__(self, x: X) -> Y |
x & self |
CanRXor[X, Y] |
__rxor__(self, x: X) -> Y |
x ^ self |
CanROr[X, Y] |
__ror__(self, x: X) -> Y |
x | self |
Note that CanRPow
corresponds to CanPow2
; the 3-parameter "modulo" pow
does not reflect in Python.
Similarly, the augmented assignment operators are described by the following
optype
interfaces:
Type | Signature | Expression |
---|---|---|
CanIAdd[X, Y] |
__iadd__(self, x: X) -> Y |
self += x |
CanISub[X, Y] |
__isub__(self, x: X) -> Y |
self -= x |
CanIMul[X, Y] |
__imul__(self, x: X) -> Y |
self *= x |
CanIMatmul[X, Y] |
__imatmul__(self, x: X) -> Y |
self @= x |
CanITruediv[X, Y] |
__itruediv__(self, x: X) -> Y |
self /= x |
CanIFloordiv[X, Y] |
__ifloordiv__(self, x: X) -> Y |
self //= x |
CanIMod[X, Y] |
__imod__(self, x: X) -> Y |
self %= x |
CanIPow[X, Y] |
__ipow__(self, x: X) -> Y |
self **= x |
CanILshift[X, Y] |
__ilshift__(self, x: X) -> Y |
self <<= x |
CanIRshift[X, Y] |
__irshift__(self, x: X) -> Y |
self >>= x |
CanIAnd[X, Y] |
__iand__(self, x: X) -> Y |
self &= x |
CanIXor[X, Y] |
__ixor__(self, x: X) -> Y |
self ^= x |
CanIOr[X, Y] |
__ior__(self, x: X) -> Y |
self |= x |
Additionally, there are the unary arithmetic operators:
Type | Signature | Expression |
---|---|---|
CanPos[Y] |
__pos__(self) -> Y |
+self |
CanNeg[Y] |
__neg__(self) -> Y |
-self |
CanInvert[Y] |
__invert__(self) -> Y |
~self |
CanAbs[Y] |
__abs__(self) -> Y |
abs(self) |
The round
function comes in two flavors, and their overloaded intersection:
Type | Signature | Expression |
---|---|---|
CanRound1[Y1] |
__round__(self) -> Y1 |
round(self) |
CanRound2[N, Y2] |
__round__(self, n: N) -> Y2 |
round(self, n) |
CanRound[N, Y1, Y2] |
__round__(self) -> Y1 __round__(self, n: N) -> Y2
|
round(self[, n: N]) |
The last "double" signature denotes overloading.
To illustrate; float
is a CanRound[int, int, float]
and int
a
CanRound[int, int, int]
.
And finally, the remaining rounding functions:
Type | Signature | Expression |
---|---|---|
CanTrunc[Y] |
__trunc__(self) -> Y |
math.trunc(self) |
CanFloor[Y] |
__floor__(self) -> Y |
math.floor(self) |
CanCeil[Y] |
__ceil__(self) -> Y |
math.ceil(self) |
Note that the type parameter Y
is unbounded, because technically these
methods can return any type.
Context managers
Support for the with
statement.
Type | Signature |
---|---|
CanEnter[V] |
__enter__(self) -> V |
CanExit[R] |
__exit__(self, *exc_info: *ExcInfo) -> R |
In case of errors, the type alias ExcInfo
will be
tuple[type[E], E, types.TracebackType]
, where E
is some BaseException
.
On the other hand, if no errors are raised (without being silenced),
then Excinfo
will be None
in triplicate.
Because everyone that enters must also leave (that means you too, Barry),
optype
provides the intersection type
CanWith[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:
class CanWith[V, R](CanEnter[V], CanExit[R]):
# You won't find any bugs here :)
...
Buffer types
Interfaces for emulating buffer types.
Type | Signature |
---|---|
CanBuffer[B: int] |
__buffer__(self, flags: B) -> memoryview |
CanReleaseBuffer |
__release_buffer__(self) -> None |
The flags: B
parameter accepts integers within the [1, 1023]
interval.
Note that the CanReleaseBuffer
isn't always needed.
See the Python docs or inspect.BufferFlags
for more info.
Async objects
The optype
variant of collections.abc.Awaitable[V]
. The only difference
is that optype.CanAwait[V]
is a pure interface, whereas Awaitable
is
also an abstract base class.
Type | Signature | Expression |
---|---|---|
CanAwait[V] |
__await__(self) -> Generator[Any, None, V] |
await self |
Async Iteration
Yes, you guessed it right; the abracadabra collections repeated their mistakes with their async iterablors (or something like that).
But fret not, the optype
alternatives are right here:
Type | Signature | Expression |
---|---|---|
CanAnext[V] |
__anext__(self) -> V |
anext(self) |
CanAiter[Vs: CanAnext] |
__aiter__(self) -> Y |
aiter(self) |
But wait, shouldn't V
be a CanAwait
? Well, only if you don't want to get
fired...
Technically speaking, __anext__
can return any type, and anext
will 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 managers
Support for the async with
statement.
Type | Signature |
---|---|
CanAenter[V] |
__aenter__(self) -> CanAwait[V] |
CanAexit[R] |
__aexit__(self, *exc_info: *ExcInfo) -> CanAwait[R] |
And just like CanWith[V, R]
for sync context managers,
there is the CanAsyncWith[V, R] = CanAenter[V] & CanAexit[R]
intersection
type.
Future plans
- Support for Python versions before 3.12.
- A drop-in replacement for the
operator
standard 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 ops
GeFromLt
,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 for
numpy
's array interface.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file optype-0.1.0.tar.gz
.
File metadata
- Download URL: optype-0.1.0.tar.gz
- Upload date:
- Size: 19.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.7.1 CPython/3.11.5 Linux/6.6.10-76060610-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 99e9b3d9cedc94c2e3a7467ed5ac6d5195574310884bdf057e1181c4854023ad |
|
MD5 | 7571253e6d590016509522ec88fe7491 |
|
BLAKE2b-256 | 5bbb39627cc47b137691d15e5ae6b1324d87ae0227f68660cd2b133e98c30103 |
File details
Details for the file optype-0.1.0-py3-none-any.whl
.
File metadata
- Download URL: optype-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.7.1 CPython/3.11.5 Linux/6.6.10-76060610-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 54b7864b38ee63649f72780f5c316314ce5694f8e724e259de26514d8a1bfea9 |
|
MD5 | 9cd7c8ee2c6ce1fa2d934945cbc52fde |
|
BLAKE2b-256 | ec3bef1094b8bd7480fc5752ecf9414489781f77dfbc4351ecb8e134ab714962 |