Numerary
A pure-Python codified rant aspiring to a world where numbers and types can work together.
Install / Use
/learn @beartype/NumeraryREADME
Copyright and other protections apply.
Please see the accompanying LICENSE file 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 just ints and floats?
Are you annotating that interface for documentation and type-checking?
Were you excited by PEP 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 number 3186 haunt you in your dreams?
Do you find yourself shouting to no one in particular, “There has to 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 own ~~out of sheer desperation~~ from first principles! If you still have no idea what I’m talking about, this may help illustrate.
numerary is a pure-Python codified rant for signaling that your interface is usable with non-native numeric primitives[^1] without breaking type-checking.
More simply, numerary aspires 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.
numerary is licensed under the MIT License.
See the accompanying LICENSE file for details.
It should be considered experimental for now, but should settle down quickly.
See the release notes for a summary of version-to-version changes.
Source code is available on GitHub.
If you find it lacking in any way, please don’t hesitate to bring it to my attention.
You had me at, “numbers and types can work together”
numerary strives to define composable, efficient protocols 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:
>>> from numerary import IntegralLike, RealLike
>>> def deeper_thot(arg: RealLike) -> IntegralLike:
... assert arg != 0 and arg ** 0 == 1
... return arg // arg + 42
Beyond default compositions for common use cases, numerary expands on the Supports pattern used in the standard library.
For example, numerary.types.SupportsIntegralOps is a @typing.runtime_checkable protocol that approximates the unary and binary operators introduced by numbers.Integral.
>>> from numerary.types import SupportsIntegralOps
>>> def shift_right_one(arg: SupportsIntegralOps) -> SupportsIntegralOps:
... assert isinstance(arg, SupportsIntegralOps)
... return arg >> 1
>>> shift_right_one(2)
1
>>> from sympy import sympify
>>> two = sympify("2") ; type(two)
<class 'sympy.core.numbers.Integer'>
>>> res = shift_right_one(two) ; res
1
>>> type(res)
<class 'sympy.core.numbers.One'>
>>> from fractions import Fraction
>>> shift_right_one(Fraction(1, 2)) # type: ignore [arg-type] # properly caught by Mypy
Traceback (most recent call last):
...
AssertionError
!!! note
Until 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’s Supports protocols 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__.
>>> from typing import TypeVar
>>> T_co = TypeVar("T_co", covariant=True)
>>> from numerary.types import (
... CachingProtocolMeta, Protocol, runtime_checkable,
... SupportsAbs, SupportsDivmod,
... )
>>> @runtime_checkable
... class MyType(
... 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)
>>> from fractions import Fraction
>>> my_type = Fraction(22, 7)
>>> isinstance(my_type, MyType)
True
>>> abs(my_type)
Fraction(22, 7)
>>> divmod(my_type, 2)
(1, Fraction(8, 7))
>>> from decimal import Decimal
>>> 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)
False
Remember that scandal where complex defined 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 ends here.
>>> from numerary.types import SupportsRealOps
>>> isinstance(1.0, SupportsRealOps) # all good
True
>>> has_real_ops: SupportsRealOps = complex(1) # type: ignore [assignment] # properly caught by Mypy
>>> isinstance(complex(1), SupportsRealOps) # you're not fooling anyone, buddy
False
numerary not only caches runtime protocol evaluations, but allows overriding those evaluations when the default machinery gets it wrong.
>>> from abc import abstractmethod
>>> from numerary.types import CachingProtocolMeta, Protocol, runtime_checkable
>>> @runtime_checkable
... class MySupportsOne(Protocol, metaclass=CachingProtocolMeta):
... @abstractmethod
... def one(self) -> int:
... pass
>>> class Imposter:
... def one(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 again
False
``num
