Inspiration: mypy/issues#3186. Please see that issue for a rich discussion and some very useful context and history. It’s long, but worth the time if you care about this issue. This post picks up from that thread.
I think solving this use case I identified in #3186 would go a long way:
I want to publish a library that does math on numeric primitives. I want to provide algorithms. I want my customers to be able to supply their own number implementations. I want to support typing annotations so that my customers can use existing tools to know whether their supplied number primitives satisfy interoperability requirements of my library. I also want to be able to check this at runtime in my library.
High bit: I think it’s important to define and enforce flavors of numerics as generic APIs. Builtin types should be compliant implementations.
At a high level, I think this involves:
- Building something that allows for defining generic numerics and their operations. I’m pretty sure the consensus is that Mypy’s protocols are (currently) insufficient for this purpose.
- Replace or simplify the numeric tower to comply with common operations (existing dunder methods seem reasonable). Establish explicit conversion/promotion APIs (both generic flavors and to builtin types). Minimize and explicitly and generically type implicit conversions like with
__truediv__
and__pow__
. - Bring both implementation and type definitions into compliance for all standard library primitives.
Mapping flavors onto a class hierarchy so far seems problematic, but it may be possible with care. I don’t think a class hierarchy should be a requirement. (I.e., we shouldn’t be afraid to ditch PEP3141.) Well-defined conversion/promotion APIs may suffice as an (albeit potentially complicated) alternative. I think the standard library is in a good position to define at least FloatT
, RationalT
, IntegerT
(and maybe ComplexT
), but if it does, builtin types (including Decimal
and Fraction
) should be compliant and should validate against those APIs.
I think achieving this in the standard library works benefits beyond just enabling generic algorithms to work with compliant numeric implementations. Additionally, it would act as a forcing function for internal consistency between numeric typing and numeric implementations. Further, it could role model techniques for third party math and science packages that promote interoperable extensions.
__truediv__
presents an oddity where an operator involving a single flavor (IntegerT
) can result in a different flavor (IntegerT / IntegerT -> RationalT
or IntegerT / IntegerT -> FloatT
as it is currently). __pow__
presents additional sharp edges (IntegerT ** RationalT -> FloatT
). Those and similar cases can probably be accommodated with care. @overload
s may end up fairly complicated, but that may be an acceptable price to pay. Having clear lossy-vs-lossless conversion/promotion interfaces will likely help.
I don’t think it’s necessary to require interoperability between numeric implementations, but I think if you solve the above problem, you’ll get a lot of that anyway, especially if you enforce the presence of conversion/promotion interfaces like __float__
, __trunc__
, __int__
, __floor__
, __ceil__
, etc. (maybe add __numerator__
, __denominator__
with a default implementation for IntegerT
, etc.). Numeric implementations could rely on Supports…
interfaces to type and perform conversions/promotions before performing their operations. Or they could provide their own conversion/promotion implementations (e.g., sympy.sympify
, which I believe is called implicitly to allow things like sympy.Symbol("x") + numpy.int64(0)
to work). That being said, I think it’s really important that conversion/promotion APIs are clear when those are lossy vs. lossless. (SupportsInt
for example is ambiguous. float.__int__
is potentially lossy. numpy.int64.__int__
is not.)