This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

Author tfish2
Recipients AlexWaygood, JelleZijlstra, rhettinger, steven.daprano, tfish2
Date 2022-04-08.09:57:34
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1649411855.24.0.411672247995.issue47234@roundup.psfhosted.org>
In-reply-to
Content
This is not a partial duplicate of https://bugs.python.org/issue47121 about math.isfinite().
The problem there is about a specific function on which the documentation may be off -
I'll comment separately on that.


The problem here is: There is a semantic discrepancy between what the
term 'float' means "at run time", such as in a check like:

issubclass(type(x), float)

(I am deliberately writing it that way, given that isinstance() can, in general [but actually not for float], lie.)

and what the term 'float' means in a statically-checkable type annotation like:

def f(x: float) -> ... : ...

...and this causes headaches.


The specific example ('middle_mean') illustrates the sort of weird
situations that arise due to this. (I discovered this recently when
updating some of our company's Python onboarding material, where the
aspiration naturally is to be extremely accurate with all claims.)

So, basically, there is a choice to make between these options:

Option A: Give up on the idea that "we want to be able to reason with
stringency about the behavior of code" / "we accept that there will be
gaps between what code does and what we can reason about".  (Not
really an option, especially with an eye on "writing secure code
requires being able to reason out everything with stringency".)

Option B: Accept the discrepancy and tell people that they have to be
mindful about float-the-dynamic-type being a different concept from
float-the-static-type.

Option C: Realizing that having "float" mean different things for
dynamic and static typing was not a great idea to begin with, and get
everybody who wants to state things such as "this function parameter
can be any instance of a real number type" to use the type
`numbers.Real` instead (which may well need better support by
tooling), respectively express "can be int or float" as `Union[int,
float]`.

Also, there is Option D: PEP-484 has quite a lot of other problems
where the design does not meet rather natural requirements, such as:
"I cannot introduce a newtype for 'a mapping where I know the key to
be a particular enum-type, but the value is type-parametric'
(so the new type would also be 1-parameter type-parametric)", and
this float-mess is merely one symptom of "maybe PEP-484 was approved
too hastily and should have been also scrutinized by people
from a community with more static typing experience".


Basically, Option B would spell out as: 'We expect users who use
static type annotations to write code like this, and expect them to be
aware of the fact that the four places where the term "float" occurs
refer to two different concepts':

def foo(x: float) -> float:
  """Returns the foo of the number `x`.

  Args:
    x: float, the number to foo.

  Returns:
    float, the value of the foo-function at `x`.
  """
  ...

...which actually is shorthand for...:

def foo(x: float  # Note: means float-or-int
  ) -> float  # Note: means float-or-int
  :
  """Returns the foo of the number `x`.

  Args:
    x: the number to foo, an instance of the `float` type.

  Returns:
    The value of the foo-function at `x`,
    as an instance of the `float` type.
  """
  ...

Option C (and perhaps D) appear - to me - to be the only viable
choices here. The pain with Option C is that it invalidates/changes
the meaning of already-written code that claims to follow PEP-484,
and the main point of Option D is all about: "If we have to cause
a new wound and open up the patient again, let's try to minimize
the number of times we have to do this."

Option C would amount to changing the meaning of...:

def foo(x: float) -> float:
  """Returns the foo of the number `x`.

  Args:
    x: float, the number to foo.

  Returns:
    float, the value of the foo-function at `x`.
  """
  ...

to "static type annotation float really means instance-of-float here"
(I do note that issubclass(numpy.float64, float), so passing a
numpy-float64 is expected to work here, which is good), and ask people
who would want to have functions that can process more generic real
numbers to announce this properly. So, we would end up with basically
a list of different things that a function-sketch like the one above
could turn into - depending on the author's intentions for
the function, some major cases being perhaps:

(a) ("this is supposed to strictly operate on float")
def foo(x: float) -> float:
  """Returns the foo of the number `x`.

  Args:
    x: the number to foo.

  Returns:
    the value of the foo-function at `x`.
  """

(b) ("this will eat any kind of real number")

def foo(x: numbers.Real) -> numbers.Real:
  """Returns the foo of the number `x`.

  Args:
    x: the number to foo.

  Returns:
    the value of the foo-function at `x`.
  """

(c) ("this will eat any kind of real number, but the result will always be float")

def foo(x: numbers.Real) -> float:
  """Returns the foo of the number `x`.

  Args:
    x: the number to foo.

  Returns:
    the value of the foo-function at `x`.
  """

(d) ("this will eat int or float, but the result will always be float")

def foo(x: Union[int, float]) -> float:
  """Returns the foo of the number `x`.

  Args:
    x: the number to foo.

  Returns:
    the value of the foo-function at `x`.
  """

(e) ("this will eat int or float, and the result will also be of that type")

def foo(x: Union[int, float]) -> Union[int, float]:
  """Returns the foo of the number `x`.

  Args:
    x: the number to foo.

  Returns:
    the value of the foo-function at `x`.
  """

(f) ("this method maps a float to a real number, but subclasses can
     generalize this to accept more than float, and return something that
     gives more guarantees than being a real number.")


def myfoo(self, x: float) -> numbers.Real:
  """Returns the foo of the number `x`.

  Args:
    x: the number to foo.

  Returns:
    the value of the foo-function at `x`.
  """
History
Date User Action Args
2022-04-08 09:57:35tfish2setrecipients: + tfish2, rhettinger, steven.daprano, JelleZijlstra, AlexWaygood
2022-04-08 09:57:35tfish2setmessageid: <1649411855.24.0.411672247995.issue47234@roundup.psfhosted.org>
2022-04-08 09:57:35tfish2linkissue47234 messages
2022-04-08 09:57:34tfish2create