Python and the infinite
A recent proposal on the python-ideas mailing list would add a new way to represent floating-point infinity in the language. Cade Brown suggested the change; he cited a few different reasons for it, including fixing an inconsistency in the way the string representation of infinity is handled in the language. The discussion that followed branched in a few directions, including adding a constant for "not a number" (NaN) and a more general discussion of the inconsistent way that Python handles expressions that evaluate to infinity.
In general, Python handles floating-point numbers, including concepts like
infinity, following the standards laid out by IEEE 754. Positive and negative
infinity are represented by two
specific floating-point values in most architectures. Currently,
representing a floating-point infinite value in Python can be done using a
couple of different mechanisms. There is the float()
function, which can be passed the string "inf" to produce infinity, and there
is the
inf constant in the math library,
which is equivalent to float('inf'). Brown provided several reasons
why he believed a new, identical, and built-in constant was necessary. One of
his reasons was that he felt that infinity is a "fundamental
constant
" that should be accessible from Python without having to call
a function or require a library import.
To highlight the issue, Brown provided an example using the repr() and eval() functions. The repr() function converts a data structure to a printable string that in many cases can be evaluated back into the original data structure using eval(). Brown noted that, unlike other floating-point values, using eval() on the printable representation of inf results in an exception unless inf has been imported from math:
>>> eval(repr(float('inf')))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'inf' is not defined
Brown's post created a mega-thread around how floating-point infinity values
should be handled in Python. To some, it wasn't clear why importing
inf was a bad solution to Brown's problem. Additionally, Greg Ewing
noted that using the eval() function in the way suggested by
Brown can be "a serious security problem in some contexts.
" The
eval() function evaluates a string as Python code, which is
dangerous if the string contains user-supplied data. Ewing suggested that
the safer function to use would be literal_eval()
from the ast
library, which only evaluates literal values. Using that function would only
work, however, if inf and nan were built-in constants.
Christopher Barker
agreed, but noted another issue: "there is no way to use
literal_eval that gets you an inf (or NaN value). Which is a real, though
maybe not important, use case.
" Later in the thread, Barker
further refocused the conversation on the fundamental point that Brown's
post was leading to:
What's special is that we have a literal syntax for only a few special fundamental types: floats, ints, strings. (I [think] that's it, yes?), as well as "display" versions of a few core container types: list, dict, etc.
So the limitation here is that floats have a literal, and can have a huge number of values, all of which can be created by a literal, except inf,[-inf], and NaN.
Barker did not think that it was a "critically important
" issue,
but he did feel that extending the concept of a valid literal for the
float type to include inf and nan was useful. He
also claimed that it shouldn't require a new keyword. Barker admitted that if
a new keyword were the only way to implement Brown's idea, it would be
"dead in the water.
" Ewing
suggested a possible implementation using a "special bytecode that
looks it up as a name, and if that doesn't work, returns
float('inf'),
" but noted that it was more straightforward and
functionally the same to move math.inf and math.nan to
__builtins__.
The problem with adding these constants to __builtins__, as Ewing
noted, was that Guido van Rossum
was not in favor of that idea. To that point, Barker
responded by stating that, while Van Rossum has a "very well
respected opinion
", he is no longer the Benevolent Dictator for
Life who could make this decision unilaterally.
Barker
clarified that his goal was "to do that little bit more to make
Python better support the float special values out of the box.
" He
noted that, while PEP
754 ("IEEE 754 Floating Point Special Values
") was rejected
due to inactivity, almost all of the suggestions it made had ultimately been
implemented, except for a version of the built-in literals for infinity and
NaN now being proposed.
Van Rossum
responded by expressing his willingness to sponsor a PEP for the Python steering council to consider.
While he did not personally agree with the idea, he committed to remaining
neutral so that Barker and Brown could receive "a fair hearing
"
with that body. With Van Rossum's sponsorship support, the duo put together a
draft PEP
that is now available on GitHub. Reflecting the thread, the PEP proposes
"the introduction of built-in variables for floating point 'Infinity'
(inf) and 'Not a Number' (nan).
" Also included in the draft is an
example summarizing the impact of the proposed change:
>>> inf
inf
>>> inf > 1e9
True
>>> eval('inf')
inf
>>> assert inf == eval('inf')
>>> inf == eval(repr(inf))
True
>>> import ast
>>> inf == ast.literal_eval('inf')
True
The discussion that evolved from Brown's post did more than lead to a new PEP; it also sparked an interesting conversation around how Python handles expressions that evaluate to mathematical infinity. Paul Bryan provided a relevant example of some quirks of the IEEE specification, specifically how subtracting infinity from itself results in NaN, but comparing the NaN from that operation doesn't equal math.nan:
>>> math.inf - math.inf nan >>> (math.inf - math.inf) == math.nan False
Steven D'Aprano explained this strange behavior. According to IEEE 754, NaN values are, by definition, never equal to each other. To be otherwise, D'Aprano explained, would result in nonsensical expressions like sqrt(-2) == sqrt(-3) evaluating to True.
The conversation
prompted a question from Stephen J. Turnbull, who asked if "base" Python
could "create an infinity.
" Van Rossum had
suggested earlier in the thread that you could produce inf from
the value 1e1000 "in a pinch
". However, Turnbull noted
that a value like 1e1000, which cannot be represented in IEEE
floating point, is technically an overflow rather than an infinity. While
IEEE 754 permits mapping an overflow to infinity, that is not the same thing
as the abstract mathematical concept of infinity. In
another message, he expanded on this idea:
As Steven [D'Aprano] points out, [1e1000 is] an overflow, and IEEE *but not Python* is clear about that. In fact, none of the actual infinities I've tried (1.0 / 0.0 and math.tan(math.pi / 2.0)) result in values of inf. The former raises ZeroDivisionError and the latter gives the finite value 1.633123935319537e+16.
Turnbull explained that arithmetic involving infinite values only makes sense if it is carefully analyzed in the context of a specific calculation. The problem being that Python is inconsistent on the matter: some expressions should evaluate as the IEEE infinity but do not, some currently evaluate to inf when they are really overflows, and still others raise exceptions. As Turnbull said:
I prefer to think of it as being honest: this isn't infinity, this is overflow — and the way Python treats infs, here be Dragons, all bets are off, "magic is loose in the world", and anything can happen.
Ben Rudiak-Gould provided some
examples of Python's inconsistencies when dealing with various
mathematical expressions that represent an infinity. Each one should ideally
evaluate to inf, but most raised exceptions like ValueError
or OverflowError instead; "I get the impression that little
planning has gone into this
", he added.
Brown
agreed that "a look should be taken at the structure of math-based
errors and exceptions,
" adding that "the exceptions make little
sense.
" Since the types of changes required to make Python consistent
in this regard would represent a significant backward-compatibility break, it
seems unlikely that they will be addressed soon.
As for the PEP, the next step is for Brown and Barker to submit a pull request to the steering council for acceptance, be assigned a PEP number, and bring it up again for further discussion. Before the steering council or its delegate will be able to accept or reject it, though, the PEP will need to be discussed on the python-dev mailing list. Time will tell if Brown's wish to move inf and nan into __builtins__ is accepted, and what, if anything, comes of the inconsistent way that Python handles concepts like infinity throughout the language.
| Index entries for this article | |
|---|---|
| Python | Floating point |
