Skip to content

Cannot instantiate subclasses (including newtypes) of floats #527

Open
@jesboat

Description

Consider the following example:

from typing import NewType
T = NewType('T', float)
T(1.0)

Pyre expands the NewType definition into:

class T(float):
    def __init__(self, input: float) -> None: pass

which fails with:

example.py:3:0 Invalid class instantiation [45]:
  Cannot instantiate abstract class `T` with abstract methods `__ceil__`, `__floor__`.

This is a very awkward case. Python's float is subclass of ABC numbers.Real (which is hardwired into Pyre The relevant declarations in typeshed (irrelevant methods elided, ...s in original) are:

class Real(Complex, SupportsFloat):
    @abstractmethod
    def __floor__(self) -> int: ...
    @abstractmethod
    def __ceil__(self) -> int: ...

class float:
    if sys.version_info >= (3, 9):
        def __ceil__(self) -> int: ...
        def __floor__(self) -> int: ...

(from numbers.pyi and builtins.pyi)

In fact, float on CPython 3.7 and 3.8 does appear to be missing __ceil__ and __float__ methods. This doesn't break users because the exposed API for ceil/floor is through the functions math.ceil and math.floor which handle floats internally and only delegate to __ceil__ and __floor__ when called on a non-float.

This doesn't cause Pyre to explode normally presumably because AbstractClassInstantiation failures are internally suppressed for int/float/bool. But, that suppression trick fails for subclasses of builtins.

This is a kinda gross situation, and there are no obvious good fixes. Some ideas:

  1. Argue that float should properly be considered an abstract type (pre-3.9), since allowing instances would mean that someFloat.__ceil__() would pass the typechecker but fail at runtime. Take a hard-line approach and raise type errors. This is obviously insane.

  2. Argue that float should properly be considered an abstract type (pre-3.9). Admit that it's impractical to prevent people from instantiating floats, and say that the minimum breakage would be to allow instantiation of float itself but nothing else. (This is the status quo.)

  3. Attempt to have Pyre capture the actual Python behavior, which is something like "requiring instantiation of subclasses of Real (except float or its subclasses) to define __ceil__ and __floor__, but treat attempts to use __ceil__ or __floor__ as type errors. This seems like a lot of implementation hacks for relatively little benefit.

  4. Remove __ceil__ and __floor__ from Real. This basically just moves the problem to math.floor and math.ceil by making them do an unchecked cast of their argument from Real to Union[float, subclass of Real which defines __ceil__ and __floor__].

  5. Modify typeshed to pretend float defines __ceil__ and __floor__ for all Python versions. This seems minimally objectionable. Pyre already thinks float.__ceil__ and float.__floor__ are ok because float <: Real. All it changes is extending the "I can be an instance of Real without actually implementing __ceil__/__floor__ as long as math.ceil and math.floor still work" privilege from float to float subclasses.

Of these, I'd lean towards option 5, and I'll put a diff up for that. There might be some better idea I didn't think of.

Thoughts?

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions