Open antonagestam opened 2 years ago
It makes sense to me to allow inf
, -inf
, and nan
as literal values. After all, we allow all other float values as literal values as well.
After all, we allow all other float values as literal values as well.
We don't. PEP 586 lists all valid types for literals, and float isn't among them.
Yes, float literals are not supported.
Also, call expressions are not (nor should be) allowed in type annotations. It's important that we keep type annotation expressions simple — for reasons of consistency and evaluation performance.
An approach that avoids call expressions could be to allow Literal[math.inf]
(and Literal[math.nan]
?). Type checkers would then have to special case math.inf
as referring to float("inf")
.
Not sure this is worth changing in the type system though. In the OP's case, they could just write limit: int | float
(or limit: float
).
I currently am designing an api which looks like:
def redirect_to(self, somewhere_else: Self, * for: timedelta | Literal[float("inf")]): ...
Because I want to make sure people don't have to deal with the units they are passing. Specifying this is in seconds I think at least is a bit annoying especially if the endpoint I'm calling changes it's units without warning.
@JelleZijlstra Just a nitpick but "they could just write limit: int | float
", that wouldn't be equivalent as that wouldn't give a type error for less_than(1, 5.5)
(I really only wanted to allow ints and infinity, not all floats).
@erictraut That makes sense, would it be less complex to do what Jelle suggests? Although I realize this feature would probably have a quite low "return on investment" in terms of gained typing accuracy per time invested in building and specifying it.
My code has float("-inf")
bugs 😎
Just my 2 cents, I am no professional on the matter. Could inf
, +inf
, -inf
and nan
(with names that are actually usable as types) be type aliases of float? And special-cased by type checkers that supports them. Or maybe a new PEP needs to be suggested 🤷
ie:
NegativeInfinity: TypeAlias = float
PositiveInfinity: TypeAlias = float
Infinity: NegativeInfinity | PositiveInfinity
NotANumber: TypeAlias = float
# idk if this is even feasable, or even needed.
# Maybe it could be useful in some arithmetic cases, or when delaying.
# Reminds me of LiteralString for its safety applications.
FiniteFloat: TypeAlias = float
float("-inf") # type: NegativeInfinity # type: Literal[NegativeInfinity]
float("+inf") # type: PositiveInfinity # type: Literal[PositiveInfinity]
float("inf") # type: PositiveInfinity # type: Literal[PositiveInfinity]
float("nan") # type: NotANumber # type: Literal[NotANumber]
I agree this is "low return on investment". Still something I would like as a type user.
For OP's use case, maybe a "comparable-to-int" protocol could work? This is probably a good type to choose for the pyyaml PR that got linked here
@hauntsaninja No that would not have worked, as already repeated, the real usecase really required int.
Here is my use case for the same feature request:
I am the author of NoGraphs, a library dealing with implicit graphs. Here, the application can choose what data types to use (including rare choices like Decimal or arbitrary precision floats), optionally made type-safe by type annotations.
A common choice for graph applications is to use int for edge weights and distances. But just because an infinity value is needed as default, the annotation needs to be int | float (or just float). This is disturbing from the perspective of the application, because it uses int to exclude precision problems - and then, float occurs "everywhere" in the application...
(I hope, this can be helpful for the discussion - I am no specialist in the topic. I might be completely wrong...)
Perhaps a better way to handle such an API is to use a dedicated sentinel value rather than float("inf")
to indicate the default. The builtin symbol None
is often used as such a sentinel. Ellipsis
can also work for this purpose. The value of float("inf")
is not a good sentinel because it cannot be described unambiguously in the type system.
I think so, too. Thanks. None
or Ellipsis
are valid alternatives.
I also agree with the perspective w.r.t. the type system. Furthermore, in the use case, Optional[int]
with None
as sentinel is less disturbing than int | float
, if avoiding precision problems is the goal.
In some cases, these alternatives might also have disadvantages: Since int
are not comparable to NoneType
or EllipsisType
, whilst 5 < float("infinity")
works, the new sentinel has to be handled as special case in all comparisons in the package (here: in each graph algorithm at several places). And in the area of graph algorithms, the literature nearly always uses the term "infinity" for the default (ok, in textbooks and wikipedia, and just in this area...). And the application might expect to deal with distances, including infinite distance, and now, it also has to deal with None
as special case. And in stackoverflow, obscure proposals like to use inf = cast(int, math.inf)
or to subclass int
come up, in order to better live with the situation.
Although: Solving the issue might just be not worth it, or too complicated, or would have side effects... (My perspective and knowledge about such topics is very limited. And I can perfectly live without inf as literal.)
@HeWeMel You can create a new nographs.Infinity
type based on float("inf")
(https://docs.python.org/3/library/typing.html#newtype) and expose an instance of it as a helper constant (nographs.INFINITY
for example). Similarly to how the math library has math.inf
.
Then type your parameters as int | nographs.Infinity
(this has the advantage of letting the users know when they can use Infinity or not).
Then your runtime checks basically stay the same as you can check for float("inf")
, which avoids breaking old code and untyped code.
This way users of your library who also want strict typing can pass an instance of nographs.Infinity
(which is just a float("inf")
) and users who don't use type-checkers can also pass float("inf")
interchangeably.
That being said, I would also prefer being able to simply use a literal infinity directly from python's type system. 😄
@Avasam, this sounds like a very good idea. I will definitely try this. Thanks!
@erictraut Please note that such usage of None is acknowledged in the original post of this topic.
I do hear you that it's not worth spoiling the otherwise simpler syntax of current type hints for this rather special case though. It's a minor flaw that I think we can live with.
In mathematical contexts `inf
is a relatively common default:
So a Literal[inf]
would be very useful in those contexts.
How about using enum?
class Bound(float, enum.Enum):
Inf = float("inf")
NegInf = float("-inf")
Constants could be easily extracted via Inf: Final = Bound.Inf
.
Literal wrapping and comparison also works.
This would really only be practical if the enums were "batteries included" in base python. Having every library define their own Enum and then, for correct type checking, require users to use that specific implementation is burdensome. I've tried and abandoned this in personal projects.
This would really only be practical if the enums were "batteries included" in base python. Having every library define their own Enum and then, for correct type checking, require users to use that specific implementation is burdensome. I've tried and abandoned this in personal projects.
Agree. I considered adding this to typing
module, and annotate that float.__new__
will return this enum. However float("inf")
creates different objects, which violates enum's usage (allowing identification with is
)
As already said, using positive infinity as a default value for integers is a very common and natural choice.
Using types in Python has for me two benefits:
Writing
def my_code(value: int | float = math.inf):
...
while valid, is both less explicit, and less safe than int | Literal[math.inf]
.
On the other hand, using None
as a sentinel value is safe but less explicit and leads to more verbose and slighter less readable code.
Perhaps using a custom Enum
as suggested by @BlueGlassBlock is the cleaner option at this stage, at least if user is not supposed to enter default infinity value manually, since forcing users to use our custom infinity wrapper is not so nice.
Generally speaking, infinities are treated as second class citizens in Python (I had to patch ast.literal_eval()
to support them for example...).
Since Python is so often used by scientists, having a better support for them would be really nice (at least, float.inf
instead of having to import math, or ideally a builtin name, as suggested in the past, or Infinity()
as a subclass of float
...), but this is beyond the scope of this issue. ;)
`
I have been frequently using positive and negative infinity as default values for values that should otherwise be
int
s. Because of Python's duck typing this is a convenient pattern that sometimes allows reducing some special-casing logic.vs
Would it be feasible to special-case the expressions
float("inf")
andfloat("-inf")
and make type-checkers regard them as literal values, even though they aren't strictly speaking language-level literals?For posterity, PEP 586 mentions this with:
So I guess what I'm asking here is: am I an odd duckling or could it be worth considering +/- infinity as legal values in literal types?