python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.22k stars 2.78k forks source link

int is not a Number? #3186

Closed tjltjl closed 1 year ago

tjltjl commented 7 years ago

n : Number = 5 produces

test_compiler.py:18: error: Incompatible types in assignment (expression has type "int", variable has type "Number")

Which it probably shouldn't because isinstance(n, Number) == True

uhasker commented 2 years ago

Ran into the same issue. Basically we have a class containing a dictionary which represents a matching. The dictionary values are expected to (sometimes) come from numpy functions, so they might be int or np.int.

Trying to do this:

from numbers import Integral
from typing import Dict
import numpy as np

class SomeClassWithADict:
    def __init__(self, d: Dict[Integral, Integral]):
        self.d = d

some = SomeClassWithADict({0: 1})
some2 = SomeClassWithADict({np.int64(0): np.int64(1)})

results in the issue that seems relevant to this discussion:

example.py:9: error: Dict entry 0 has incompatible type "int": "int"; expected "Integral": "Integral"
example.py:10: error: Dict entry 0 has incompatible type "signedinteger[_64Bit]": "signedinteger[_64Bit]"; expected "Integral": "Integral"

I (kind of) understand the reason for this & that there are various workarounds as I've read this discussion, but the above example seems like something that should work. Are there still plans to do something about support for the numeric tower in the future or should the expectation be that we have to live with the workarounds?

posita commented 2 years ago

@uhasker, I think some of your issue can be reduced to the following:

# test_numpy_int64_as_integral.py
from numbers import Integral
import numpy as np

int64_int: int = np.int64(0)  # fails validation
int64_integral: Integral = np.int64(0)  # also fails validation
assert issubclass(np.int64, Integral)  # succeeds
assert isinstance(np.int64(0), Integral)  # also succeeds

The above now results in the following errors (which aren't very insightful):

test_numpy_int64_as_integral.py:5: error: Incompatible types in assignment (expression has type "signedinteger[_64Bit]", variable has type "int")  [assignment]
test_numpy_int64_as_integral.py:6: error: Incompatible types in assignment (expression has type "signedinteger[_64Bit]", variable has type "Integral")  [assignment]

Somewhere along the way, Mypy stopped validating numpy's integral primitives against numbers.Integral. I don't know whether that was Mypy's doing, or numpy's, or a combination of things. I do know that Mypy no longer validates numpy.int64s (among other things) against numerary's IntegralLike protocol, at least in part because numpy's comparators (e.g., numpy.int64.__gt__) are substantially broader than expected.

# test_numpy_int64_with_integral_like.py
import numpy as np
from numerary import IntegralLike

int64_val: IntegralLike = np.int64(0)  # used to work, now it blows up

The above now results in the following errors:

test_numpy_int64_with_integral_like.py:5: error: Incompatible types in assignment (expression has type "signedinteger[_64Bit]", variable has type "IntegralLike[Any]")  [assignment]
test_numpy_int64_with_integral_like.py:5: note: Following member(s) of "signedinteger[_64Bit]" have conflicts:
test_numpy_int64_with_integral_like.py:5: note:     __ge__: expected "Callable[[Any], bool]", got "_ComparisonOp[Union[int, float, complex, number[Any], bool_], Union[_SupportsArray[dtype[Union[bool_, number[Any]]]], _NestedSequence[_SupportsArray[dtype[Union[bool_, number[Any]]]]], bool, int, float, complex, _NestedSequence[Union[bool, int, float, complex]]]]"
test_numpy_int64_with_integral_like.py:5: note:     __gt__: expected "Callable[[Any], bool]", got "_ComparisonOp[Union[int, float, complex, number[Any], bool_], Union[_SupportsArray[dtype[Union[bool_, number[Any]]]], _NestedSequence[_SupportsArray[dtype[Union[bool_, number[Any]]]]], bool, int, float, complex, _NestedSequence[Union[bool, int, float, complex]]]]"
test_numpy_int64_with_integral_like.py:5: note:     <4 more conflict(s) not shown>
Found 1 error in 1 file (checked 1 source file)

The unfortunate reality (measured in deeds, not words) is that Python does not support provide sufficient guidance or APIs for supporting interoperable numeric implementations. Period. Nor does it seek to anymore. They are abandoned in practice. The thing that is really irksome is that primary sources (i.e., the standard library docs, various PEPs, etc.) give the dangerously false impression to many, many users that it does, and that one should be able to do (and successfully type check) precisely the kind of thing you've done. Many thousands (maybe tens of thousands) of wasted developer hours does not seem to be a motivating factor to highlight this gap for newcomers. Everyone must learn the hard way, again, and again, and again.

Frequently, the observation is made that Python is (largely) a volunteer effort and one should jump in and fix things, but (for the outsider, at least) this is a trap. The process is byzantine, the effort immense, and the issues have largely been litigated in favor of doing nothing. The tragedy is that no one is willing to come clean, kill the numeric tower, and explicitly note in the standard library that type-checked numeric implementation interoperability isn't supported, and probably won't ever be.

gvanrossum commented 2 years ago

I agree the numeric tower is unlikely to be properly implemented, either in mypy or in CPython. If you are serious about killing it, I think there is a way forward. Start a discussion (either on python-dev or on discuss.python.org) with a subject like "Let's kill the numbers module" (or "numeric tower"), and aim to write a PEP explaining the issues and proposing to revert PEP 3141. That will get the issue in front of the SC.

s-m-e commented 2 years ago

Frequently, the observation is made that Python is (largely) a volunteer effort and one should jump in and fix things, but this is a trap. The process is byzantine, the effort immense, and the issues have largely been litigated in favor of doing nothing.

I am a little surprised by this given the amount of money currently circulating in the Python ecosystem i.e. with its many commercial users. The question comes down to use-cases. A properly implemented numeric tower, as I also once falsely assumed to exist (see dumb questions plus brilliant answers e.g. here or here), would allow a ton of interesting features in both numerical applications and testing.

The amount of stupid workarounds that I have seen more and more recently is just staggering. I might be going out on a limb here, but perhaps it is more of a question of awareness and coordination to get this resolved as opposed to simply reverting PEP 3141. Fellow R&D engineers & scientists - are you there? Wake up!

That will get the issue in front of the SC.

If a proposal to revert PEP 3141 serves to stir the discussion, then I am all for it.

posita commented 2 years ago

For the record, I fully endorse @s-m-e's sentiments:

A properly implemented numeric tower, as I also once falsely assumed to exist (see dumb questions plus brilliant answers e.g. here or here), would allow a ton of interesting features in both numerical applications and testing. [¶] The amount of stupid workarounds that I have seen more and more recently is just staggering.

I am the reluctant author of one of those stupid workarounds who wants nothing more than to celebrate its obsolescence. I also share this hope (acknowledging that hope is not a strategy):

… I might be going out on a limb here, but perhaps it is more of a question of awareness and coordination to get this resolved as opposed to simply reverting PEP 3141.

Many of us want a proper, type-checkable numeric interoperability API. Absent that, we need to stop claiming we have one.

Best: Do it well. 🥇🥳 Worthy of respect: Don't do it. 🤔😤 Worst of all possible worlds: Claim you do it, then do it poorly or not at all. 😱🤬

gvanrossum commented 2 years ago

Is the complaint just about mypy (and perhaps other static type checkers?), or is it about lack of support for the numeric tower in general (e.g. at runtime)?

NeilGirdhar commented 2 years ago

The amount of stupid workarounds that I have seen more and more recently is just staggering. I might be going out on a limb here, but perhaps it is more of a question of awareness and coordination to get this resolved as opposed to simply reverting PEP 3141. Fellow R&D engineers & scientists - are you there? Wake up!

This comment seems to outline what could be done to solve the problem: https://github.com/python/mypy/issues/2922#issuecomment-1148137581

posita commented 2 years ago

Is the complaint just about mypy (and perhaps other static type checkers?), or is it about lack of support for the numeric tower in general (e.g. at runtime)?

I think the short answer is that this is about type-checking. If generic numerics were properly definable and type-checkable (i.e., this issue had a satisfactory resolution), I don't think we'd be having this conversation.

That being said, I think the issue is more subtle. I don't think those worlds are distinct (even though they emerged independently and at different times). As @s-m-e points out, especially with a language that enjoys such popularity among the math and science crowd, numeric interoperability is valuable and should imply type-compatibility. Long running inconsistencies have led math/science package owners to no longer care about the numeric tower and instead invest in their own proprietary and often hyper-specific APIs and type definitions that don't earnestly contemplate even algorithmic interoperability. That's a lot of inertia to overcome. I think a lot of this subtlety has been captured (and recaptured) in many of this issue's prior comments and references. Some highlights:

Maybe I'm missing the trees for the forest, though.

gvanrossum commented 2 years ago

Maybe you're barking up the wrong tree. More rhetoric isn't going to change this. Mypy is being maintained by two or three Dropbox employees who occasionally are allowed to spend a little bit of time on it, if their other (Dropbox infrastructure) responsibilities allow it. They already can't keep up with new Python features and newly accepted typing PEPs. If you want mypy development to be better funded maybe you and others who actually want these features should go figure out how to get the math/science package community to offer those folks a job.

JelleZijlstra commented 2 years ago

Echoing what Guido said, what this really needs is someone to put the work into solving the problems discussed here. That may require a PEP, either to give up and deprecate the numbers module, or to propose new behavior that integrates the numeric tower better with the static type system.

I'll note that it's far from clear how to do the latter. The original complaint in this issue is that int is not assignable to Number. We could probably solve that with some special-casing, e.g. as suggested in https://github.com/python/mypy/issues/2922#issuecomment-1148137581.

But then what could you do with a Number object? As it turns out, basically nothing, because Number doesn't define any methods (https://github.com/python/typeshed/blob/ccdb558af95e0badb7aeb553d43f5346a3d65695/stdlib/numbers.pyi#L9):

class Number(metaclass=ABCMeta):
    @abstractmethod
    def __hash__(self) -> int: ...

(And even the __hash__ part contradicts PEP 3141, which explicitly allows for mutable numbers.)

Maybe Number isn't useful, but we could still use e.g. numbers.Real. Then it turns out that our current stubs are basically all Any, so you won't get much useful type checking: https://github.com/python/typeshed/blob/ccdb558af95e0badb7aeb553d43f5346a3d65695/stdlib/numbers.pyi#L52. Maybe we can improve that, for example by defining def __add__(self, other: Real) -> Real: ..., but that will require thinking carefully about the implications for subclassing. (Must a Real really accept any other Real in __add__? builtins.float doesn't.)

uhasker commented 2 years ago

@posita I agree that the issue can be simplified - I wanted to provide a clear example of why one might want to need to take "something that behaves like an int" to avoid the response "just don't do that" (frankly, I didn't think that anyone would respond to this - I am very glad that people did).

IMHO there seem to be two issues here:

  1. The number tower as outlined in PEP3141 has some problems. First I think that the Number class shouldn't exist since it doesn't and can't do anything meaningful. After all an abstract base class should communicate some valuable information about supported operations. However, Number can't do that, because there is no actual definition of a number - hence the
class Number(metaclass=ABCMeta):
    @abstractmethod
    def __hash__(self) -> int: ...

which as @JelleZijlstra pointed out isn't even conforming to PEP3141, so really it should be

class Number(metaclass=ABCMeta):
    pass

The fundamental problem is that there is AFAIK no situation where I can say "this is a number", but can't say one of the much more meaningful "this is an integer" or "this is a real number" or "this is a complex number".

Additionally there is just a bunch weird stuff like hasattr(Complex, "__le__") being True.

The number tower should probably be cleaned up to have the inheritance chain Complex :> Real :> Rational :> Integral with cleanly defined and unsurprising interfaces.

  1. After the number tower is meaningful, it probably becomes simpler (or at least possible) to fix the second issue (which is that mypy doesn't think that int is Integral and everything related to that) via defining useful stubs in numbers.pyi etc (see the comment above me).

@JelleZijlstra: If you think that the above is remotely sane, I can offer some help (I am aware that this open source and so no one really has time) - both with proposals as well as with writing actual code. I believe that this is an important issue (we use the number tower a lot) & that this should be fixed one way or another.

EDIT @JelleZijlstra: If you don't think that the above is sane, but could still use help I am of course happy to offer it anyways ;) I only care about solving this problem in some satisfying manner.

astrojuanlu commented 2 years ago

Don't think Complex :> Real :> Rational :> Integral is a useful inheritance chain, it comes with the typical LSP problems https://en.m.wikipedia.org/wiki/Circle%E2%80%93ellipse_problem e.g. numbers on the real line can be sorted, complex ones can't (edit: or maybe I understood it the other way around? But even so, I find "integer is a complex" difficult to digest and possible error prone, similar to the wiki I linked)

vnmabus commented 2 years ago

I think having a proper number tower, with Complex, Real, Rational and Integral as protocols, would be useful. I agree that the Number class is probably useless for now (what is even a number? Is anyone's code as generic as to really being able to deal with ANY number, such as quaternions or hyperreals?).

analog-cbarber commented 2 years ago

Yes, plus there are no actual Real numbers in programming. Floats are just an approximation, not a subtype. I think people think to much about just the values that can be represented by each type and not the operations that can be performed using them. You can assign an in32 to an int64 without losing information, but you cannot perform the same operations on int32 and int64 variables and always get the same answers. It isn't hard to to come up with some hierarchy based on assignability, but once you start taking operations into account it gets a lot more complicated and will depend on practice on what operations the program is going to perform and perhaps also what actual range of values it expects to encounter.

BTW, does anyone use Rational? I cannot say I have ever used such a datatype in 30+ years of coding. Why waste a lot of effort supporting types that almost no one should ever use?

In any case, unless some volunteer wants to sink a lot of time into this (with the foreknowledge that you may end up wasting a lot of time), I think the most we can expect from mypy right now is a note in the documentation or perhaps an optional warning when these abstract types are used.

uhasker commented 2 years ago

@astrojuanlu That's why Real inherits from Complex and not the other way around ;) Sorry if the notation I used was confusing - I just lifted it from the respective PEP. Unless I am missing something, LSP is a non-issue here.

But even so, I find "integer is a complex" difficult to digest and possible error prone, similar to the wiki I linked)

I definitely see your point, but have to disagree. It is naturally useful to identify every complex number of the form a+0i with the real number a (see e.g. Axler, 1995 for an example of literature that does that). For example no one wants to write (3 + 0i)(5 + 4i) - people prefer 3(5+4i). But as soon as we do that we automatically identify every natural number a with a+0i.

@vnmabus And we're not even talking about surreal numbers yet ;) The problem is simply that there is just no useful information conveyed when you say that x is a number contrary to e.g. saying that x is a float (from a programming perspective anyway).

@analog-cbarber Looks to me like the only thing that really implements Rational is Fraction & in that case you can just specify the Fraction type, so I (probably) agree.

However, I disagree that the hierachy is based on assignability - complex numbers form a field (so you can add, sub, mul & div etc), real numbers form an ordered field (so you can additionally do comparisons) etc. Therefore I would argue that the hierarchy is inherently based on operations.

EDIT since there seems to be some minor confusion here: I am ofc not claiming that actual floats form an ordered field, only that real numbers do.

I agree that it would be a lot of effort, but at least an optional warning should be in order since as they stand, the abstract number types are useless for typing anything.

vnmabus commented 2 years ago

Yes, plus there are no actual Real numbers in programming. Floats are just an approximation, not a subtype.

There are more implementations of real numbers than float. In fact, the current tower does not take this into account, as it leaves decimal outside the tower. However in a flexible enough tower you could implement floating point, arbitrary precision decimals, symbolic real representations or more esoteric representations like unums and all of them would be subclasses of Real. In order for that to work one should not require interoperability between them or with float to be a Real, IMHO.

BTW, does anyone use Rational?

Remember that Python is also used in math contexts, where having a different implementation of Rational could make sense. See for example Sage and its multiple number classes, such as Rational.

analog-cbarber commented 2 years ago

Complex numbers form a field, but actual complex floating point types as implemented in a programming language are NOT actually a field. It is convenient to think of a float as if it were the same as a Real, but technically it is not. Floats are NOT implementations of real numbers, they are approximations of real numbers, but they do NOT obey the same properties and it is a mistake to think of them as being the same thing in any formal sense.

And even if you can always assign a real to a complex (by assuming the complex portion is zero), that is NOT what you want to do in most signal processing/engineering applications that use complex numbers. It is often a sign of a bug.

A big problem here is that it is hard to define what "interoperability" even means and it may mean different things to different users in different contexts.

eric-wieser commented 2 years ago

The number tower should probably be cleaned up to have the inheritance chain Complex :> Real :> Rational :> Integral with cleanly defined and unsurprising interfaces.

One weird thing ot me about this model is that it's very complex-centric. There are other types of "number" like:

Obviously these don't have native python support, but the point in having abstract interfaces like the numeric tower is precisely to cater for types that aren't shipped with Python.

Maybe wanting extra base classes like this for "bigger" number systems can be handled with abc.register, but it feels quite unusual to extend a piece of code by injecting new base classes.

NeilGirdhar commented 2 years ago

Maybe we can improve that, for example by defining def add(self, other: Real) -> Real: ..., but that will require thinking carefully about the implications for subclassing. (Must a Real really accept any other Real in add? builtins.float doesn't.)

Thinking carefully about these questions seems to be most pressing starting point to repairing numbers. Is there a good place to start a discussion to do that?

Maybe it would be worthwhile to see if the person who (somewhat recently) did the numpy type annotations and probably had to reason about a lot of similar questions could comment?

Then, the conclusions of that discussion can hopefully be committed to the typeshed.

Finally, the other half of the work seems to be fixing #2922, which would imbue all of the ordinary numbers (int, float, complex) with the newly-decided interfaces.

posita commented 2 years ago

BTW, does anyone use Rational?

Remember that Python is also used in math contexts, where having a different implementation of Rational could make sense. See for example Sage and its multiple number classes, such as Rational.

Rationals are useful for discrete computations without being lossy like floats.

posita commented 2 years ago

Per @gvanrossum's recommendation, I started a thread on discuss.python.org to go beyond #2922 (hat tip to @NeilGirdhar).

posita commented 2 years ago

Maybe it would be worthwhile to see if the person who (somewhat recently) did the numpy type annotations and probably had to reason about a lot of similar questions could comment?

@BvB93, is that you? We are struggling here. It appears you have recently given typing a lot of thought for numpy. Would you be willing to lend your expertise and advice (perhaps here or in the discuss.python.org thread I recently started)?

mdickinson commented 2 years ago

I can offer some help (I am aware that this open source and so no one really has time) - both with proposals as well as with writing actual code

I'd love to see proposals for a reworked numeric tower based on typing use-cases; I don't have a huge amount in the way of time and energy to offer, but would be willing to sponsor a PEP, or at the very least to review a PEP authored / sponsored by others.

I agree with others posting here that the numbers module isn't a great fit for typing needs; that's not terribly surprising, given that it came into existence long before serious Python typing efforts. I think it also suffered from a lack of clear use-cases to drive its development. Given that we now have a decent selection of real world use-cases, developing something new that fits those use-cases feels like the right way forward - we're in a much better position now to determine what's useful and what isn't.

Trying to mutate or adapt the existing numbers module to fit the needs of the typing community seems like a much harder proposition, not least because published interfaces are horribly rigid (add anything new and you break implementers of that interface; remove anything and you break the users). I don't see a strong need to deprecate PEP 3141; I'd just accept that it's not all that useful for typing purposes, ignore it and move on.

So practical next steps might look something like:

I think there's a set of really hard problems to do with mixed-type arithmetic that it may not be reasonable to expect any kind of typing framework to solve. Suppose I have two different Real-like classes A and B; there are all sorts of ways that mixed-mode arithmetic between A and B, A and float, B and int, etc. might work or not work; it may simply not be reasonable to try to capture all of the possible permutations in type declarations. (For an example of how not to do it, take a look at the GAP framework. But this at least demonstrates that there's a hard problem to solve.)

FWIW, I've done a fair amount of numeric coding both at home (mostly number-theoretic or concerned with esoteric floating-point stuff) and at work (mostly data-science'y and based on the standard NumPy / SciPy / tensorflow / ... scientific stack), and in the time since the numbers module was brought into existence I have yet to find it useful. What has been useful are typing protocols like SupportsIndex and SupportsFloat. (SupportsInt, not so much.) The kind of use-cases I personally have, and would love to see addressed in any new proposal, are things like "usable as a float", "usable as an int", "can be converted to a Fraction".

posita commented 2 years ago

@mdickinson, I attempted to reproduce (and respond) to your post on discuss.python.org. Apologies if imposing that venue change is disruptive. No disrespect is intended.

uhasker commented 2 years ago

Since the discussion regarding the number tower is now at discuss.python.org I would suggest that within this thread we continue to talk about a possible mypy hotfix only.

My suggestion would be (as outlined above) to have a warning that the abstract numeric base classes shouldn't be used for typing. This would mean that whenever the user attempts to use Number, Real etc, a warning along the lines of "Using abstract numeric base classes for typing is (currently) a bad idea, see e.g. issue #3186" (probably with some nicer wording) is displayed. The warning should also probably contain suggestions for workarounds (like using SupportsInt etc).

NeilGirdhar commented 2 years ago

his would mean that whenever the user attempts to use Number, Real etc, a warning along the lines of "Using abstract numeric base classes for typing is (currently) a bad idea,

But it's not always a bad idea. It's fine to use it for single dispatch (as a type annotation that guides the dispatch), for example. Do we want to trade some false positives for other false positives?

The ideal thing would be to detect the error that int isn't matching Integral, for example. However, that might be harder than just implementing #2922 so that it does.

cfcohen commented 2 years ago

I commented on this ticket (or one of its many duplicates) a couple of years ago. I've been trying to cobble together a library that interoperates between a number of domains (e.g. floats, Fraction, Decimal, numpy, gmp, sympy, etc.). Because the interfaces for these domains are so inconsistent, I desperately need the assistance of mypy type checking to alert me to differences in the APIs of the various libraries. e.g. Sympy doesn't support trunc(). I don't really see how to get there practically without the ability to place related implementations in a group with a name like numbers.Real. I could have invented my own, but that didn't completely solve the problem either because I need some cooperation from the language on int, float, etc. And that primarily came from the poorly supported PEP3141.

Sensing that the commitment to supporting PEP3141 was weak at best, about two years ago I produced my own mypy patch, with the expectation that I would muddle through until this issue was resolved in some more principled way. Two years later, it's less than clear that a consensus solution will ever emerge...

So, here's what I did: https://github.com/python/mypy/commit/09d83670bd8c060c1c7cb4db21b2c5a44abfe25a

I don't claim that this fix is correct for any definition, just that it works for me, and I think it might be helpful for others. I'm open to commentary on why it's the wrong fix or discussion about why this kind of a solution shouldn't be adopted more broadly, which will probably be rooted in some reasonable argument about half-solutions. I also welcome feedback of the form "It would be better if it did this instead." The reason why I haven't proposed any serious solutions to the problem is exactly because I know how thorny the problem really is, and how inadequate my knowledge is to truly solve it. That said, practical problems need practical solutions, and as hacky as this patch is, it was the solution that worked for me. At least for a while. I'm less thrilled about maintaining it forever...

oscarbenjamin commented 2 years ago

e.g. Sympy doesn't support trunc()

As far as I can tell trunc works with SymPy:

>>> import sympy
>>> e = sympy.sqrt(2)
>>> e
sqrt(2)
>>> import math
>>> math.trunc(e)
1

Please open a SymPy issue if I've misunderstood (the thread here is long enough that it doesn't need any further discussion on this point).

theRealProHacker commented 2 years ago

If I may add the discussion.

1. Solution Protocols

As already stated, using Number as a protocol doesn't really make sense right now, because it doesn't tell you what a Number should be doing (it doesn't have any members). One solution would be to change that and add some common functions like

I think the real problem is that in the documentation it says that all numbers inherit from Number, which is wrong. So I suggest we should fix that. And I totally agree with @tjltjl

I (quite reasonably?) thought that isinstance(x, cls) implies that you can have the

 x : cls

The above implication not holding true is IMO not acceptable for a proper language. And I feel like there is not enough will to solve this issue if this is already 5 years old. I know this sounds harsh, but I just want to express how absurd this is.

Are there other places where this is not the case?

I would love this question to be answered.

2. Solution: User defines what numbers mean to them

Several comments noted that what a number really is depends on the person and the use case. Should a Vector (complex numbers are basically just 2-Vectors) be a number? Can floats be considered real?
The last question for example could be answered with no: If you'd pick a random real number, you would never pick a number that can be represented as a float
Or it could be answered with yes: floats are a good enough simulation of the real numbers and I don't really care if my result is wrong in the 9th digit because I am just using Python for education and I don't want to explain to my students why floats aren't actually real.

In conclusion, we could just say everyone defines for themselves what a number is. This could also just be temporary solution

Here, I want to show a simple solution for most projects:

  1. Make a file called my_types.py (or whatever name that doesn't collide with other things)
  2. Write this into the file
    
    Number = int, float, Decimal # and whatever else you like
    NumberType = int | float | Decimal # I know it feels weird to write the same thing twice just with different delimiters, but this might just be the life of a python developer for some time. 

3. Now you can import Number and NumberType in another file and use them together

def test_Numbers(x: None|NumberType)->NumberType: if isinstance(x, Number): reveal_type(x) y: NumberType = x else: y = 0 return y

To make this process easier, Python could support two canonical function that convert a Union into a tuple, and the reverse and are understood by type checkers. Another similar possibility would be allowing Union types as parameter for `isinstance`. And as I see this is being developed but isn't working on my machine with Python 3.10.1 yet.
This intertwining of static and dynamic type checking is a feature that is sensible, practical, easy to understand and requested.  
For example [here](https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type)

Note: The revealed type of `x` is `Union[builtins.float, _decimal.Decimal]`. Probably because you could just write `x: float = 0`. So `int|float` is reduced to `float`.
Note 2: Looking at the above note, it feels ridiculous why ´x: float = 0` is correct but `x: Number = 0` is not.

# 3. Solution: Special case in type checking
The comment above actually shows us how to do it. Just like `float` also accepts `int` and complex allows both `float` and `int`, the `Types` `Number`, `Real`, ... should just allow the `types` where `isinstance(type(),Type)` is True. I would suggest this as an immediate hotfix. This should be pretty easy and would fix the problem. 

# 4. Solution: Nominal Subtyping
The easiest and maybe just the best way to do it is good old nominal subtyping. Just like before, it is decided on a case to case basis what a number is. However, not by the end user, but by the library makers (The Python language just gives guidelines on what a `Number` should provide)  
Right now, there is an incoherence between the theoretical state of the language and the practical state. Theoretically, `int` should be a subtype of `Number` by the definition of `Number` in theory/in the documentation. From my understanding, `isinstance` knows from `int.__instancecheck__` that int is a subtype of `Number` even though it isn't actually: `int.__mro__ == (<class 'int'>, <class 'object'>)`. 

Making theory and practice coherent again would mean to make `int` actually subclass `Number`. `Numpy` could then for example just subclass from Numbers too. If a user has a special use-case where the library definition of a Number isn't good for him, he can still just use solution 2 and make his own number class. 

# Summary
## Hotfix
- Add special cases to mypy so that the fundamental typing rules aren't violated anymore
- Developers using numbers like the issue creator (`x: Number = 0`) should be provided with the information that they can just use `x: float = 0`. This will probably solve most further issues raised from inexperienced developers that might for example come from JavaScript, where `Numbers` is just one type that mangles `ints` and `floats` together.
- Change the documentation of the numbers module

## Additional Notes to Prototyping
What shouldn't happen is that we get types like `UnsignedNumber` or `IndivisableNumber` and small splitters of Number types that are difficult to put in a simple tree structure. 
An Example where extreme subtyping for example is extremely annoying (it might seem off-topic, but it's just an example):
```python
# I define this here. 
style_mapping = Mapping[str, Style] # Style could be anything

# somewhere totally different
d1: style_mapping = {
  "example": Style()
}
# somewhere different again, maybe in a function
d2: style_mapping = {
  "example": Style()
}
d3 = ChainMap(d1, d2)
# error because ChainMap wants to have a MutableMapping

I will never edit anything in this ChainMap. It's just for viewing the dicts without copying them. There would be different solutions to this problem (eg I could make an ImmutableChainMap). Some might even seem simple, but they all involve me writing more code just because the ChainMap only takes a MutableMapping, when really it could also just take an immutable mapping with no problem and just raise an error when it gets changed.

I would really love some feedback on these ideas. I can also take some hits 😁. However, I believe that it would make sense to decide on one primary idea because
There should be one– and preferably only one –obvious way to do it.

NeilGirdhar commented 2 years ago

@theRealProHacker It's also been pointed out in a few places that the number 3 solution here: https://github.com/python/mypy/issues/2922#issuecomment-283038234 would solve the Numbers problem. This would require no special cases. You would just register the appropriate numerical types and they would work.

Solution 1: protocols

I feel like the reason people want to use protocols is because protocols get around the registration problem. The protocol is automatically bound as a parent class. But the issue is that protocols are really inferior to explicit registration. You'd have to be so careful when designing your class to match the protocol, and the protocol could never change or else everyone counting on the automatic registration would suddenly find that their numerical class isn't a number anymore. It's a bad solution.

Solution 2

This is what I do now, but it's not a good solution compared with just fixing the registration.

  1. Solution: Special case in type checking

Would be fine as a hotfix. I still think the registration fix would be the best long term solution.

Solution 4: int is a subtype of Number even though it isn't actually:

int is registered as a subtype, which is good enough. Unfortunately, the registration isn't visible to MyPy. But it is visible at runtime.

Making theory and practice coherent again would mean to make int actually subclass Number.

I don't think that's necessary (or even desirable). Registration is just as good as inheritance.

cfcohen commented 2 years ago

I agree that ABCMeta.register() support https://github.com/python/mypy/issues/2922 is a better solution than the one I used, and I had assumed that at some point in the future it would be the fix that actually resolved this issue. I just couldn't figure out how to make that change myself. If I've correctly understood the solutions proposed in that issue, approach three would provide a practically workable solution without introducing performance problems or creating complex multi-pass algorithms. Such an approach would be required to support my other extensions to Numbers (see https://github.com/cfcohen/mypy/commit/eee71cad035e61f58f76d33fd848119a75da044e).

As for the comment on sympy, it wasn't meant to be a bug report -- it was just an example of the most recent problem that mypy had helped me find. While math.trunc() works for number-like values, it doesn't work for symbols, producing TypeError: can't truncate symbols and expressions and from the sympy stubs I'm using: error: Argument 1 to "trunc" has incompatible type "Basic"; expected "SupportsTrunc". It would have been convenient for me if Trunc() was an operator like Add() tor Pow() in sympy. While this sympy behavior isn't that surprising in retrospect, it's easy to get confused by the observation above where math.trunc() does work for some sympy values when combined with the mypy type Union[int, float, fractions.Fraction, decimal.Decimal, Numbers.Rational, sympy.Basic] to miss the detail about trunc() not being supported on symbols. I commented on this despite the length of this thread because I think it helps elucidate the value of having a more robust definition of what methods are required on which classes in the Numbers tower. I was surprised for example, that there seems to be no requirement that Integers are Rationals are ordered. :-( Perhaps this aspect of the discussion belongs in the Python thread about what to do about PEP3141.

theRealProHacker commented 2 years ago

I experimented a bit and tried to build a plugin as a hotfix. I would suggest that either this or https://github.com/python/mypy/commit/09d83670bd8c060c1c7cb4db21b2c5a44abfe25a should be used as a hotfix. Additionally, I would still suggest that if someone uses anything from numbers he is linked to https://peps.python.org/pep-0484/#the-numeric-tower. Something like:

Note: Instead of using the numbers module check out peps.python.org/pep-0484/#the-numeric-tower

I think that would be a huge and simple improvement right now. 😁

from mypy.plugin import Plugin as _Plugin, ClassDefContext, TypeInfo
import numbers
from numbers import __all__
from decimal import Decimal
import os

tdata: dict[str, tuple[type, int]] = {
    f"numbers.{cls}":(getattr(numbers, cls), -1-i) for i,cls in enumerate(__all__)
}

bdata: dict[str, type] = { # these are all the builtin numbers I am aware of
    "builtins.int": int,
    "builtins.float": float,
    "builtins.complex": complex,
    "_decimal.Decimal": Decimal
}

bstore: dict[str, TypeInfo] = {}
tstore: dict[str, TypeInfo] = {}

_key = "number-plugin-done"
def is_done(info: TypeInfo):
    rv = _key in info.metadata
    info.metadata[_key] = {}
    return rv

abspath = os.path.abspath(__file__)
prep = "import sys\n"

class Plugin(_Plugin):
    refreshed = False
    def get_customize_class_mro_hook(self, name: str):
        # refreshing to trick mypys caching. That however leads to inefficiency.
        # So this should be integrated into mypy in such a way that it runs every 
        # time without forcing everything else to restart
        if not self.refreshed:
            self.refreshed = True
            with open(abspath) as file:
                s = file.read()
            with open(abspath, "w") as file:
                file.write(s.removeprefix(prep) if s.startswith(prep) \
                    else prep + s)
        # handling Numbers
        if name.startswith("numbers."):
            tcls, index = tdata[name]
            def tinner(ctx: ClassDefContext):
                tinfo = ctx.cls.info
                if is_done(tinfo): return
                tstore[name] = tinfo
                for bname, binfo in bstore.items():
                    if not issubclass(bdata[bname], tcls): break
                    binfo.mro.insert(index, tinfo)
            return tinner
        # handling numbers
        if not name in bdata: return
        def binner(ctx: ClassDefContext):
            binfo = ctx.cls.info
            if is_done(binfo): return
            bstore[name] = binfo
            bcls = bdata[name]
            for tname, tinfo in tstore.items():
                tcls, index = tdata[tname]
                if not issubclass(bcls, tcls): continue
                binfo.mro.insert(index, tinfo)
        return binner

def plugin(version: str):
    # TODO:
    # if version >= fix_version: return _Plugin
    return Plugin

Additionally, a small test file that should run without errors.

from numbers import Number, Complex
from decimal import Decimal

class OwnNumber(int): pass

x: Number = 0

y: list[Number] = [
    1, 1.0, 1+0j, Decimal(1.0), OwnNumber(1.0)
]

def main(x: Number|str)->str:
    if isinstance(x,Number):
        return str(x).replace(".",",")
    else:
        return x.strip()

def add_numbers(x: Number, y: Number):
    # theoretically numbers have no attributes you can rely on but practically 
    # we know that they will probably be interoperable with other numbers
    # this ignore is required because of the theoretic aspect. 
    # We need to settle on one thing that all numbers should have in common. 
    # I would suggest that they must have a possibility to convert to a `complex`, `float` *or* `int` 
    # and thats it. Everything else is optional because it can be deduced.
    return x + y # type: ignore 

def add_numbers_(x: Complex, y: Complex)->Complex:
    # this works although the revealed type of x+y is Any
    # here I would suggest that type.__add__(anything) should always return type
    # for example adding a Complex to a Real should result in a Complex
    return x+y
tjltjl commented 2 years ago

As the original reporter of the bug, I wholeheartedly agree on the suggestion "if anyone uses anything from numbers, ...". This would have fixed the original problem right away.

hauntsaninja commented 1 year ago

As of #15137, mypy will now issue a much more helpful diagnostic when this comes up.

There is no plan to make naive use of numbers.Number type check: 1) no major python static type checker supports ABC registration, see e.g. https://github.com/python/mypy/issues/2922 2) you'd need to lie a bunch to make things work because there are incompatibilities in the classes see e.g. https://github.com/python/mypy/issues/3186#issuecomment-762121456 3) the end result would still be very unsound and so the value proposition is dicey, see e.g. https://github.com/python/mypy/issues/3186#issuecomment-1176882591

If you have a use case, I recommend using your own Protocol to describe exactly what behaviour you expect.

If for some reason a Protocol cannot do what you want it to do, please open an issue at https://github.com/python/typing

If you're interested in designing some future of interoperability of numeric types or discussing the future of the numbers module, this thread https://discuss.python.org/t/numeric-generics-where-do-we-go-from-pep-3141-and-present-day-mypy/17155/12 is a good place to start.

Since there are no clear action items for mypy that don't have better more specific issues tracking them, I'm going to take the bold step of closing this issue.

This is a long issue with a lot of history. If you feel the need to post something, please read this thread through first.