Package: numerary
Copyright and other protections apply.
Please see the accompanyingLICENSEfile for rights and restrictions governing use of this software.
All rights not expressly waived or licensed are reserved.
If that file is missing or appears to be modified from its original, then please contact the author before viewing or using this software in any capacity.Are you defining a numeric interface that should work with more than justints andfloats?
Are you annotating that interface for documentationandtype-checking?
Were you excited byPEP 3141’s glitz and gloss promising a clean, straightforward number type definition mechanism, only to learn the hard way—after many hours of searching, tweaking, hacking, and testing ever more convoluted code, again and again—that you could’t actually make it work with Python’s type-checking system?
Do you now wonder whether numbers were something new to computing in general because nothing else would explain such a gaping hole in a programming language so popular with the STEM crowd that has been around since the early 1990s?
Does the number3186haunt you in your dreams?
Do you find yourself shouting to no one in particular, “Therehasto be a better way?”Well I’m here to tell you there isn’t.
But until there is, there’s …numerary—Now with Protocol Power™That’s right!For a hopefully limited time, you too can benefit from someone else’s deranged work-arounds for the enormous chasms in Python that lie between the esoteric fields of computation that are “typing” and “numbers” instead of having to roll your ownout of sheer desperationfrom first principles!
If you still have no idea what I’m talking about,this may help illustrate.numeraryis a pure-Python codified rant for signaling that your interface is usable with non-native numeric primitives[^1] without breaking type-checking.
More simply,numeraryaspires to a world where numbers and types can work together.If you’re thinking that you shouldn’t need a 🤬ing library for that,you’re right.[^1]:You know, *super* weird, off-the-wall shit, like members of the [numeric tower](https://docs.python.org/3/library/numbers.html), or [standard library primitives that remain *non*-members for some 🤬ed up reason](https://docs.python.org/3/library/decimal.html), or [legitimate non-members because they predate PEP 3141 and conforming would amount to breaking changes](https://trac.sagemath.org/ticket/28234), or—I don’t know—oodles of libraries and applications that have been around for literally decades that bring huge value to vast scientific and mathematic audiences, but whose number primitives break type-checking if one abides by the ubiquitous bum steer, “I don’t have any experience trying to do what you’re doing, but just use ``float``, bro.”

Because, hey, *🤬* numbers!
Am I right?This madness should enjoy no audience.
It should not exist.
Yet here we are.
Its author gauges its success by how quickly it can be forgotten, relegated to the annals of superfluous folly.numeraryis licensed under theMIT License.
See the accompanyingLICENSEfile for details.
It should be considered experimental for now, but should settle down quickly.
See therelease notesfor a summary of version-to-version changes.
Source code isavailable on GitHub.If you find it lacking in any way, please don’t hesitate tobring it to my attention.You had me at, “numbers and types can work together”numerarystrives to define composable,efficientprotocols that one can use to construct numeric requirements.
If all you deal with are integrals and reals, and what you want is broad arithmetic operator compatibility, this will probably get you where you likely want to go:>>>fromnumeraryimportIntegralLike,RealLike>>>defdeeper_thot(arg:RealLike)->IntegralLike:...assertarg!=0andarg**0==1...returnarg//arg+42Beyond default compositions for common use cases,numeraryexpands on theSupportspatternused in the standard library.
For example,numerary.types.SupportsIntegralOpsis a@typing.runtime_checkableprotocol that approximates the unary and binary operators introduced bynumbers.Integral.>>>fromnumerary.typesimportSupportsIntegralOps>>>defshift_right_one(arg:SupportsIntegralOps)->SupportsIntegralOps:...assertisinstance(arg,SupportsIntegralOps)...returnarg>>1>>>shift_right_one(2)1>>>fromsympyimportsympify>>>two=sympify("2");type(two)<class'sympy.core.numbers.Integer'>>>>res=shift_right_one(two);res1>>>type(res)<class'sympy.core.numbers.One'>>>>fromfractionsimportFraction>>>shift_right_one(Fraction(1,2))# type: ignore [arg-type]  # properly caught by MypyTraceback(mostrecentcalllast):...AssertionError!!! noteUntil 1.9, ``sympy.Integer`` [lacked the requisite bitwise operators](https://github.com/sympy/sympy/issues/19311).
``numerary`` catches that!
The above properly results in both a type-checking error as well as a runtime failure for [SymPy](https://www.sympy.org/) versions prior to 1.9.numerary’sSupportsprotocols can be composed to refine requirements.
For example, let’s say one wanted to ensure type compatibility with primitives that support both__abs__and__divmod__.>>>fromtypingimportTypeVar>>>T_co=TypeVar("T_co",covariant=True)>>>fromnumerary.typesimport(...CachingProtocolMeta,Protocol,runtime_checkable,...SupportsAbs,SupportsDivmod,...)>>>@runtime_checkable...classMyType(...SupportsAbs[T_co],SupportsDivmod[T_co],...Protocol,metaclass=CachingProtocolMeta,...):...pass>>>my_type:MyType>>>my_type=3.5>>>isinstance(my_type,MyType)True>>>abs(my_type)3.5>>>divmod(my_type,2)(1.0,1.5)>>>fromfractionsimportFraction>>>my_type=Fraction(22,7)>>>isinstance(my_type,MyType)True>>>abs(my_type)Fraction(22,7)>>>divmod(my_type,2)(1,Fraction(8,7))>>>fromdecimalimportDecimal>>>my_type=Decimal("5.2")>>>isinstance(my_type,MyType)True>>>abs(my_type)Decimal('5.2')>>>divmod(my_type,2)(Decimal('2'),Decimal('1.2'))>>>my_type="nope"# type: ignore [assignment]  # properly caught by Mypy>>>isinstance(my_type,MyType)FalseRemember that scandal wherecomplexdefined exception-throwing comparators it wasn’t supposed to have, which confused runtime protocol checking, and then its type definitions lied about it to cover it up?
Yeah, that shit endshere.>>>fromnumerary.typesimportSupportsRealOps>>>isinstance(1.0,SupportsRealOps)# all goodTrue>>>has_real_ops:SupportsRealOps=complex(1)# type: ignore [assignment]  # properly caught by Mypy>>>isinstance(complex(1),SupportsRealOps)# you're not fooling anyone, buddyFalsenumerarynot only caches runtime protocol evaluations, but allows overriding those evaluations when the default machinery gets it wrong.>>>fromabcimportabstractmethod>>>fromnumerary.typesimportCachingProtocolMeta,Protocol,runtime_checkable>>>@runtime_checkable...classMySupportsOne(Protocol,metaclass=CachingProtocolMeta):...@abstractmethod...defone(self)->int:...pass>>>classImposter:...defone(self)->str:...return"one">>>imp:MySupportsOne=Imposter()# type: ignore [assignment]  # properly caught by Mypy>>>isinstance(imp,MySupportsOne)# fool me once, shame on you ...True>>>MySupportsOne.excludes(Imposter)>>>isinstance(imp,MySupportsOne)# ... can't get fooled againFalsenumeraryhas default overrides to correct for known oddities with native types (like our old friend,complex) and with popular libraries likenumpy[^2] andsympy.
Others will be added as they are identified.
If I’ve missed any, or if you would likenumeraryto support additional number implementations out of the box, pleaselet me know.[^2]:!!! bug

    `numpy` no longer validates on assignment as it once did.
    See [posita/numerary#16](https://github.com/posita/numerary/issues/16) for details.Performance Enhanced Protocols—Adifferentkind of “PEP” for your stepBy default,protocols frustrate runtime type-checking performance.A lot.numeraryapplies two distinct, layered optimization strategies:Cached__instancecheck__results fornumerary-defined protocols; andOptional(-ish) short-circuit type enumerations.Cached__instancecheck__resultsTo understand whynumeraryprotocols are faster for runtime checks, it helps to understand why non-numeraryprotocols are so slow.
At runtime (i.e., viaisinstance), thedefaultProtocolimplementationdelegates totype(Protocol).__instancecheck__to perform a crude comparison of an instance’s callable attributes against the protocol’s.
More attributes means more comparisons.
Further, it performs these comparisons … Every. Single. 🤬ing. Time.Protocols provided bynumeraryuse insteadCachingProtocolMetaas their meta class.CachingProtocolMetaderives fromtype(beartype.typing.Protocol)which caches results based on instance type.numerary’s version allows for runtime check overrides of those results.Conceptually:>>>isinstance(1,SupportsIntegralOps)# first check for an int is delegated to type(Protocol).__instancecheck__True>>>isinstance(2,SupportsIntegralOps)# cached resultTrue>>>isinstance(1.0,SupportsIntegralOps)# the first check for a float is delegated to type(Protocol).__instancecheck__False>>>isinstance(2.0,SupportsIntegralOps)# cached resultFalseThese offer significant performance improvements, especially where protocols define many methods.--8<--"docs/perf_supports_complex.txt"Source:perf_supports_complex.ipy--8<--"docs/perf_supports_complex.ipy"Unions for inclusionSometimes we might want types that don’t comply with protocol definitions to validate anyway (e.g., because we know they will work at runtime).
For example,floats in Python versions prior to 3.9 officially lacked__floor__and__ceil__methods, but were registered with the numeric tower and worked just fine withmath.floorandmath.ceil.How doesnumerary’sSupportsFloorCeildeal with this situation?
Not very well, unfortunately, at least not on its own.>>>importmath,sys>>>fromnumerary.typesimportSupportsFloorCeil>>>defmy_dumb_floor_func(arg:SupportsFloorCeil)->int:...assertisinstance(arg,SupportsFloorCeil)# will work, even for floats, thanks to default overrides...returnmath.floor(arg)# type: ignore [arg-type]  # doesn't understand SupportsFloorCeil>>>float_val:float=1.6180339887>>># For illustration only until <https://github.com/python/mypy/issues/5940> is fixed>>>ifsys.version_info<(3,9):...my_dumb_floor_func(float_val)# type: ignore [arg-type]  # still results in a Mypy error for Python version <3.9...else:...my_dumb_floor_func(float_val)# validates1Unions allow a work-around.>>>fromtypingimportUnion>>>fromnumerary.typesimportSupportsFloorCeil,__floor__>>>SupportsFloorCeilU=Union[float,SupportsFloorCeil]>>>importsys>>>defmy_floor_func(arg:SupportsFloorCeilU)->int:...assertisinstance(arg,SupportsFloorCeil)...return__floor__(arg)>>>my_floor_func(float(1.2))# works in 3.8+1This is largely a contrived example, sincemath.floorandmath.ceilhappily acceptSupportsFloat, but it is useful for illustration.LimitationsThere are some downsides, though.
(Aren’t there always?)Sometimes protocols are too trustingProtocols trust numeric tower registrations.TODO(@posita): Is this really true?But sometimes, out there in the real world, implementationslie.Consider:>>>fromnumbersimportIntegral>>>hasattr(Integral,"real")andhasattr(Integral,"imag")True>>>importsympy>>>pants_on_fire=sympy.Integer(1)>>>isinstance(pants_on_fire,Integral)True>>>hasattr(pants_on_fire,"real")orhasattr(pants_on_fire,"imag")# somebody's tellin' storiesFalse>>>fromnumerary.typesimportSupportsRealImag>>>real_imag:SupportsRealImag=pants_on_fire# fails to detect the lie>>>real_imag.realTraceback(mostrecentcalllast):...AttributeError:'One'objecthasnoattribute'real'In this particular case,numeraryprovides us with a defensive mechanism.>>>fromnumerary.typesimportSupportsRealImagMixedU,real,imag>>>real_imag_defense:SupportsRealImagMixedU=pants_on_fire>>>real(real_imag_defense)1>>>imag(real_imag)0Protocols loses fidelity during runtime checkingAt runtime, protocols matchnames, notsignatures.
For example,SupportsNumeratorDenominator’snumeratoranddenominatorpropertieswill matchsage.rings.integer.Integer’s similarly namedfunctions.
In other words,isinstance(sage_integer, SupportsNumeratorDenominator)will returnTrue.
Further, if the short-circuiting approach is used, becausesage.rings.integer.Integerregisters itself with the numeric tower, thismay[^3] not be caught by Mypy.[^3]:I say *may* because I don’t really understand how Sage’s number registrations work.>>>classSageLikeRational:...def__init__(self,numerator:int,denominator:int=1):...self._numerator=numerator...self._denominator=denominator...defnumerator(self)->int:...returnself._numerator...defdenominator(self)->int:...returnself._denominator>>>fromnumerary.typesimportSupportsNumeratorDenominator>>>frac:SupportsNumeratorDenominator=Fraction(29,3)# no typing error>>>sage_rational1:SupportsNumeratorDenominator=SageLikeRational(29,3)# type: ignore [assignment]  # Mypy catches this>>>isinstance(sage_rational1,SupportsNumeratorDenominator)# isinstance does notTrue>>>sage_rational1.numerator<...method...numerator...>>>>frac.numerator29Known wartscouldbe cured by cache overriding as discussed above.
However, to combat this particular situation,numeraryprovides an alternative: theSupportsNumeratorDenominatorMethodsprotocol and thenumeratoranddenominatorhelper functions.
These accommodate rational implementations like Sage’s that are mostly compliant with the exception of their respectivenumeratoranddenominatorimplementations.>>>fromnumerary.typesimportnumerator>>>numerator(sage_rational1)29>>>numerator(frac)29>>>fromnumerary.typesimportSupportsNumeratorDenominatorMethods,numerator>>>sage_rational2:SupportsNumeratorDenominatorMethods=SageLikeRational(3,29)# no type error>>>numerator(sage_rational2)3numeraryalso defines:SupportsNumeratorDenominatorMixedU=Union[SupportsNumeratorDenominator,SupportsNumeratorDenominatorMethods,]SupportsNumeratorDenominatorMixedT=(SupportsNumeratorDenominator,SupportsNumeratorDenominatorMethods,)>>>fromnumerary.typesimportSupportsNumeratorDenominatorMixedU,numerator>>>chimera_rational:SupportsNumeratorDenominatorMixedU>>>chimera_rational=Fraction(29,3)# no type error>>>numerator(chimera_rational)29>>>chimera_rational=SageLikeRational(3,29)# still no type error>>>numerator(chimera_rational)3TheSupportsNumeratorDenominator*primitives provide the basis for analogousnumerary.types.RationalLike*primitives, whichshouldprovide sufficient (if idiosyncratic) coverage for dealing with (seemingly mis-appropriately named) rationals.Pass-through caching with composition is pretty sketchyThis is really getting into where the sausage is made, but full transparency is important, becauseCachingProtocolMetadoes change how protocols are validated at runtime.Let’s say we register an errant implementation as non-compliant using theCachingProtocolMeta.excludesmethod.>>>fromnumerary.typesimportSupportsFloat>>>classFloatImposter:...def__float__(self)->float:...raiseNotImplementedError("Haha! JK! @#$% you!")...def__int__(self)->int:...return42>>>float_imp=FloatImposter()>>>isinstance(float_imp,SupportsFloat)True>>>SupportsFloat.excludes(FloatImposter)>>>isinstance(float_imp,SupportsFloat)FalseFor registration to be ergonomic, it should be indelible, survive composition, and afford preference to subsequent overrides by inheritors.>>>fromnumerary.typesimport(...CachingProtocolMeta,Protocol,runtime_checkable,...SupportsInt,...)>>>@runtime_checkable...classMySupportsFloatInt(...SupportsFloat,SupportsInt,...Protocol,...):...pass>>>isinstance(float_imp,MySupportsFloatInt)# composition picks up override from baseFalse>>>SupportsFloat.reset_for(FloatImposter)# base resets override>>>isinstance(float_imp,SupportsFloat)True>>>isinstance(float_imp,MySupportsFloatInt)# picks up base’s changesTrue>>>MySupportsFloatInt.excludes(FloatImposter)# composition overrides>>>isinstance(float_imp,MySupportsFloatInt)False>>>SupportsFloat.includes(FloatImposter)# base changes>>>isinstance(float_imp,FloatImposter)True>>>isinstance(float_imp,MySupportsFloatInt)# composition remains unchangedFalse>>>MySupportsFloatInt.reset_for(FloatImposter)# removes override in composition>>>isinstance(float_imp,MySupportsFloatInt)# base is visible againTrue>>>SupportsFloat.excludes(FloatImposter)>>>isinstance(float_imp,MySupportsFloatInt)# base’s changes are visible to composition againFalseFor this to work under the current implementation, we cannot rely exclusively on thestandard library’s implementation of__instancecheck__, since it flattens and inspectsallproperties (with some proprietary exceptions) of all classes in the inheritance tree (in order of the MRO).
In practical terms, this means one can’t easily delegate to an ancestor’s__instancecheck__method and a protocol’s cache is effectively hidden from its progeny.
In other words, leaning on the default behavior would require one to register exceptions with every inheritor.
That would suck, so let’s not do that.However, overriding the behavior is problematic, because the standard library uses non-public interfaces to perform its attribute enumeration.
We certainly don’t want to re-implement protocol runtime checking from scratch.
(At least not yet.)beartype.typing.Protocol’s meta class tries to work around this by sneakily limiting its evaluation to directly defined attributes, and then delegatingisinstanceevaluation to its__base__classes.
In doing so, it picks up its bases’ then-cached values, but at the cost of re-implementing the attribute check as well as taking a dependency on various implementation details of the standard library, which creates a fragility.
Further, for post-inheritance updates,CachingProtocolMetaextendsbeartype’s version to implement a simplistic publish/subscribe mechanism that dirties non-overridden caches in inheritors when member protocols caches are updated.
That’s completely off the beaten path and there are probably some gremlins hiding out there.One subtlety is that the implementation deviates from performing checks in MRO order (and may perform redundant checks).
This is probably fine as long as runtime comparisons remain limited to crude checks whether attributes merely exist.
It would likely fail if runtime checking becomes more sophisticated, at which time, this implementation will need to be revisited.
Hopefully by then, we can just deletenumeraryas the aspirationally unnecessary hack it is and move on with our lives.(Seebeartype.typingandnumerary’s extensionfor details.)Licensenumeraryis licensed under theMIT License.
See the includedLICENSEfile for details.
Source code isavailable on GitHub.InstallationInstallation can be performed viaPyPI.%pipinstallnumerary
...Alternately, you can downloadthe sourceand install manually.%gitclonehttps://github.com/posita/numerary.git
...
%cdnumerary
%python-mpipinstall.# -or- python -c 'from setuptools import setup ; setup()' install ....Requirementsnumeraryrequires a relatively modern version of Python:CPython(3.8+)PyPy(CPython 3.8+ compatible)It has the following runtime dependencies:beartypefor caching protocolsnumerarywill not usebeartypeinternally unless theNUMERARY_BEARTYPEenvironment variable is set to a truthy[^4] value beforenumeraryis loaded.[^4]:I.E., one of: ``1``, ``on``, ``t``, ``true``, and ``yes``.See thehacking quick-startfor additional development and testing dependencies.Customersdyce- a pure-Python library for modeling arbitrarily complex dice mechanics andmotherbirthing code baseofnumerary!👻phantom-types- predicates and other type constraints without runtime overheadThe next one could beyou! 👋Do you have a project that suffers problems made slightly less annoying bynumerary?Let me know, and I’ll promote it here!And don’t forget to do your part in perpetuating gratuitous badge-ification!<!-- Markdown -->
As of version 0.4.1, ``dyce`` is
[![numerary-encumbered](https://raw.githubusercontent.com/posita/numerary/latest/docs/numerary-encumbered.svg)][numerary-encumbered]!
[numerary-encumbered]:https://posita.github.io/numerary/ "numerary-encumbered"..reStructuredText - see https://docutils.sourceforge.io/docs/ref/rst/directives.html#imageAs of version 0.4.1,``dyce``is |numerary-encumbered|!..|numerary-encumbered|image::https://raw.githubusercontent.com/posita/numerary/latest/docs/numerary-encumbered.svg:align:top:target:https://posita.github.io/numerary/:alt:numerary-encumbered<!-- HTML -->As of version 0.4.1,<code>dyce</code>is<ahref="https://posita.github.io/numerary/"><imgsrc="https://raw.githubusercontent.com/posita/numerary/latest/docs/numerary-encumbered.svg"alt="numerary-encumbered"style="vertical-align: middle;"></a>!