Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow float("inf") and float("-inf") in literals #1160

Open
antonagestam opened this issue Apr 22, 2022 · 21 comments
Open

Allow float("inf") and float("-inf") in literals #1160

antonagestam opened this issue Apr 22, 2022 · 21 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@antonagestam
Copy link

I have been frequently using positive and negative infinity as default values for values that should otherwise be ints. Because of Python's duck typing this is a convenient pattern that sometimes allows reducing some special-casing logic.

def less_than(value: int, limit: int | None) -> bool:
    if limit is None:
        return True
    return value < limit

vs

def less_than(value: int, limit: int | Literal[float("inf")]) -> bool:
    return value < limit

Would it be feasible to special-case the expressions float("inf") and float("-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:

Representing Literals of infinity or NaN in a clean way is tricky; real-world APIs are unlikely to vary their behavior based on a float parameter.

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?

@antonagestam antonagestam added the topic: feature Discussions about new features for Python's type annotations label Apr 22, 2022
@srittau
Copy link
Collaborator

srittau commented Apr 22, 2022

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.

@JelleZijlstra
Copy link
Member

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.

@erictraut
Copy link
Collaborator

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.

@JelleZijlstra
Copy link
Member

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).

@Gobot1234
Copy link
Contributor

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.

@antonagestam
Copy link
Author

@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.

@Kyrixty
Copy link

Kyrixty commented Jul 9, 2022

My code has float("-inf") bugs 😎

@Avasam
Copy link

Avasam commented Nov 7, 2022

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.

@hauntsaninja
Copy link
Collaborator

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

@antonagestam
Copy link
Author

@hauntsaninja No that would not have worked, as already repeated, the real usecase really required int.

@HeWeMel
Copy link

HeWeMel commented Jan 27, 2023

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...)

@erictraut
Copy link
Collaborator

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.

@HeWeMel
Copy link

HeWeMel commented Jan 27, 2023

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.)

@Avasam
Copy link

Avasam commented Jan 27, 2023

@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. 😄

@HeWeMel
Copy link

HeWeMel commented Jan 27, 2023

@Avasam, this sounds like a very good idea. I will definitely try this. Thanks!

@antonagestam
Copy link
Author

@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.

@nstarman
Copy link

In mathematical contexts ``inf` is a relatively common default:

  • specifying the norm, e.g. L-infinity
  • domains and ranges

So a Literal[inf] would be very useful in those contexts.

@BlueGlassBlock
Copy link

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.

@nstarman
Copy link

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.

@BlueGlassBlock
Copy link

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)

@wxgeo
Copy link

wxgeo commented Apr 24, 2024

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:

  • safer code, through static checking,
  • easier to read code, since typed code is much more self documenting.

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. ;)

`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests