Skip to content

Remove "unknown_format" code path in PyFloat_Pack/Unpack*() #145633

Closed

Description

@skirpichev

This is a follow-up of the #91073. Proposal was briefly discussed in the referenced issue.

Now support for IEEE floating-point formats is a requirement and the configure script will fail, if it can't detect it. The only exception is some ARM platforms (this come from b08a53a):

cpython/configure.ac

Lines 6172 to 6180 in 149c465

[*arm*], [# Some ARM platforms use a mixed-endian representation for
# doubles. While Python doesn't currently have full support
# for these platforms (see e.g., issue 1762561), we can at
# least make sure that float <-> string conversions work.
# FLOAT_WORDS_BIGENDIAN doesn't actually detect this case,
# but if it's not big or little, then it must be this?
AC_DEFINE([DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754], [1],
[Define if C doubles are 64-bit IEEE 754 binary format,
stored in ARM mixed-endian order (byte order 45670123)])],

This is only case, when "unknown_format" code patch now could be triggered (BTW, we could detect this format in runtime and switch to yet another "just copy bytes" path). Should we keep this for an unsupported platform? IIRIC, such chips aren't supported even in Debian.

With "unknown_format" we also could drop code to detect endianness in runtime. I'm not sure about float.__getformat__ function. It's docstring says:

>>> help(float.__getformat__)
Help on built-in function __getformat__:

__getformat__(typestr, /) class method of builtins.float
    You probably don't want to use this function.

      typestr
        Must be 'double' or 'float'.

    It exists mainly to be used in Python's test suite.

    This function returns whichever of 'unknown', 'IEEE, big-endian' or 'IEEE,
    little-endian' best describes the format of floating-point numbers used by the
    C type named by typestr.

Probably, it could be treated as private and we should remove it in favor of using configure macros in tests. (__set_format__() method was removed without prior deprecation in 5ab745f). Edit: we could also leave this also as-is for alternative Python implementations, using CPython tests.

Linked PRs

Activity

  1. vstinner commented on Mar 9, 2026

    @vstinner
    Member

    Before merging, should we run the PR attaches via buildbot?

    Yes, that would be a safe idea. For now, I prefer to wait until pending review comments are addressed.

  2. added a commit that references this issue on Mar 11, 2026
  3. vstinner commented on Mar 11, 2026

    @vstinner
    Member

    I don't close the issue now, since @skirpichev still plans to remove DOUBLE_IS_BIG_ENDIAN_IEEE754 and DOUBLE_IS_LITTLE_ENDIAN_IEEE754 macros.

  4. self-assigned this
    on Mar 11, 2026
  5. skirpichev commented on Mar 12, 2026

    @skirpichev
    MemberAuthor

    plans to remove DOUBLE_IS_BIG_ENDIAN_IEEE754 and DOUBLE_IS_LITTLE_ENDIAN_IEEE754 macros.

    My point is that usually (always?) endianness for floats will match endianness for other types. We could check that in the configure script and fail if this is not true. Though, it's not hard to support the current way, just for some unknown, odd, future (?) system.

    But maybe we should drop __getformat__() method?

    It's really used in tests to get the requires_IEEE_754 decorator. But I think it used incorrectly in many cases. For instance, take look on the testCopysign() in the test_math.py. This case has nothing to do with the format of floating-point numbers, but rather we expect here more strict conformance to the IEC 60559, i.e. that arithmetic operations strictly follow to the standard semantics, we assume some behavior of functions. In this case, expected certain behavior from the libm's copysign().

    This is the case for the sys.float_info.iec_60559-like flag. I think it's better to use something like this to implement the requires_IEEE_754 decorator. In few cases (something like pack/unpack tests) it will be overkill and we really need just __getformat__(). But I think it's better to assume, that pack/unpack functions will just work with IEEE formats (I'll update docs to mention this). Alternative implementations may use some tricks to make it works, like we did before.

  6. added 2 commits that reference this issue on Mar 12, 2026
  7. vstinner commented on Mar 12, 2026

    @vstinner
    Member

    But maybe we should drop __getformat__() method?

    It's used by a few projects in PyPI top 15,000 projects:

    # test.support copies
    PYPI-2026-01-07/backports_zstd-1.3.0.tar.gz: backports_zstd-1.3.0/tests/test/support/__init__.py: float.__getformat__("double").startswith("IEEE"),
    PYPI-2026-01-07/future-1.0.0.tar.gz: future-1.0.0/src/future/backports/test/support.py: #     float.__getformat__("double").startswith("IEEE"),
    PYPI-2026-01-07/zipfile36-0.1.3.tar.gz: zipfile36-0.1.3/test/support/__init__.py: float.__getformat__("double").startswith("IEEE"),
    
    # Mock
    PYPI-2026-01-07/mock-5.2.0.tar.gz: mock-5.2.0/mock/mock.py: '__getstate__', '__setstate__', '__getformat__',
    PYPI-2026-01-07/TestSlide-2.7.1.tar.gz: TestSlide-2.7.1/testslide/strict_mock.py: "__getformat__",
    
    # Type hints
    PYPI-2026-01-07/numpy-2.4.0.tar.gz: numpy-2.4.0/numpy/__init__.pyi: def __getformat__(cls, typestr: L["double", "float"], /) -> str: ...  # undocumented
    PYPI-2026-01-07/numpy-2.4.0.tar.gz: numpy-2.4.0/numpy/typing/tests/data/fail/scalars.pyi: f2.__getformat__("float")  # type: ignore[attr-defined]
    PYPI-2026-01-07/numpy-2.4.0.tar.gz: numpy-2.4.0/numpy/typing/tests/data/reveal/scalars.pyi: assert_type(f8.__getformat__("float"), str)
    
    PYPI-2026-01-07/gradio-6.2.0.tar.gz: gradio-6.2.0/venv/lib/python3.12/site-packages/numpy/__init__.pyi: def __getformat__(self, typestr: L["double", "float"], /) -> str: ...
    PYPI-2026-01-07/gradio-6.2.0.tar.gz: gradio-6.2.0/venv/lib/python3.12/site-packages/numpy/typing/tests/data/fail/scalars.pyi: f2.__getformat__("float")  # type: ignore[attr-defined]
    PYPI-2026-01-07/gradio-6.2.0.tar.gz: gradio-6.2.0/venv/lib/python3.12/site-packages/numpy/typing/tests/data/reveal/scalars.pyi: assert_type(f8.__getformat__("float"), str)
    
    # Other
    PYPI-2026-01-07/fastnumbers-5.1.1.tar.gz: fastnumbers-5.1.1/tests/test_builtin_float.py: have_getformat = hasattr(float, "__getformat__")
    PYPI-2026-01-07/fastnumbers-5.1.1.tar.gz: fastnumbers-5.1.1/tests/test_builtin_float.py: requires_getformat = unittest.skipUnless(have_getformat, "requires __getformat__")
    PYPI-2026-01-07/ypricemagic-5.1.4.tar.gz: ypricemagic-5.1.4/docs/conf.py: "__getformat__",
    
    # Mention in comments
    PYPI-2026-01-07/akshare-1.18.8.tar.gz: akshare-1.18.8/akshare/utils/demjson.py: # Could use float.__getformat__, but it is a new python feature,
    PYPI-2026-01-07/demjson3-3.0.6.tar.gz: demjson3-3.0.6/demjson3.py: # Could use float.__getformat__, but it is a new python feature,
    PYPI-2026-01-07/pygeodesy-25.12.31.tar.gz: pygeodesy-25.12.31/pygeodesy/fsums.py: # float.__getformat__('float')[:4] == 'IEEE'?)
    

    It may be safer to deprecate the method first if we plan to remove it.

  8. skirpichev commented on Mar 13, 2026

    @skirpichev
    MemberAuthor

    It's used by a few projects in PyPI top 15,000 projects

    Though, it seems in some cases they just clone CPython tests.

    numpy.float64 is compatible with builtin float, hence this method.

    It may be safer to deprecate the method first if we plan to remove it.

    Do you think it's ok to do now or we should wait fate of #138580 ?

  9. vstinner commented on Mar 13, 2026

    @vstinner
    Member

    I'm ok with deprecating float.__getformat__() right now.

  10. added a commit that references this issue on Mar 19, 2026
  11. added a commit that references this issue on Mar 19, 2026
  12. added a commit that references this issue on Mar 20, 2026
  13. skirpichev commented on Mar 25, 2026

    @skirpichev
    MemberAuthor

    I'm ok with deprecating float.getformat() right now.

    Then we need a replacement for this to implement the support.requires_IEEE_754 decorator. It seems, that the sys.float_info.iec_60559 is not a good option for this.

    We can either keep things as is, using __getformat__() to implement the decorator. No deprecations.

    OR change iec_60559 flag meaning to be true, if the float type characteristics matches the IEC 60559 double format (regardless on __STDC_IEC_559__ value). And then - use this flag in tests, deprecate __getformat__().

    OR just use some runtime checks to verify floating-point format. I think that checking sys.float_info parameters wrt the IEC double format could work in 99% cases, but we could do also direct tests like we did in the __getformat__() before, using ctypes module. And deprecate __getformat__().

    I'm going to implement the last variant.

  14. added a commit that references this issue on Mar 25, 2026
  15. removed their assignment
    on Mar 25, 2026
  16. added 2 commits that reference this issue on Mar 25, 2026
  17. encukou commented on Mar 25, 2026

    @encukou
    Member

    The s390x buildbots are broken, see e.g. here.
    It looks like it was caused by these changes.

  18. skirpichev commented on Mar 25, 2026

    @skirpichev
    MemberAuthor

    Hmm, failures start from https://buildbot.python.org/#/builders/1565/builds/4397

    I guess, it might be only #146189.

  19. vstinner commented on Mar 25, 2026

    @vstinner
    Member

    s390x is the only big endian (sys.byteorder: big) buildbot. So maybe test_struct needs to be updated?

  20. skirpichev commented on Mar 25, 2026

    @skirpichev
    MemberAuthor

    s390x is the only big endian (sys.byteorder: big) buildbot. So maybe test_struct needs to be updated?

    I don't think so.

    Looks the change reveal a bug:

    Python 3.14.3 (tags/v3.14.3:323c59a5e34, Feb 11 2026, 08:44:26) [GCC 14.2.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import math, struct
    >>> big = (1 << 25) - 1
    >>> big = math.ldexp(big, 127 - 24)
    >>> struct.pack("f", big)
    b'\x00\x00\x80\x7f'
    >>> struct.pack("<f", big)
    Traceback (most recent call last):
      File "<python-input-6>", line 1, in <module>
        struct.pack("<f", big)
        ~~~~~~~~~~~^^^^^^^^^^^
    OverflowError: float too large to pack with f format
    >>> struct.pack(">f", big)
    Traceback (most recent call last):
      File "<python-input-7>", line 1, in <module>
        struct.pack(">f", big)
        ~~~~~~~~~~~^^^^^^^^^^^
    OverflowError: float too large to pack with f format

    The problem, that native conversion does type cast (double->float), but no checks for overflows, we have correctly packed infinity:

    cpython/Modules/_struct.c

    Lines 763 to 774 in 4240c7d

    static int
    np_float(_structmodulestate *state, char *p, PyObject *v, const formatdef *f)
    {
    float x = (float)PyFloat_AsDouble(v);
    if (x == -1 && PyErr_Occurred()) {
    PyErr_SetString(state->StructError,
    "required argument is not a float");
    return -1;
    }
    memcpy(p, &x, sizeof x);
    return 0;
    }

    c.f. PyFloat_Pack4():

    cpython/Objects/floatobject.c

    Lines 2015 to 2019 in 4240c7d

    if (isinf(y) && !isinf(x)) {
    PyErr_SetString(PyExc_OverflowError,
    "float too large to pack with f format");
    return -1;
    }

    I can prepare a patch tomorrow. Else, I could suggest quickly revert #146189.

  21. encukou commented on Mar 25, 2026

    @encukou
    Member

    Thank you!
    It's a Tier-3 platform; tomorrow is fine.

  22. self-assigned this
    on Mar 25, 2026
  23. vstinner commented on Mar 25, 2026

    @vstinner
    Member

    Oh, @skirpichev already posted a more complete analysis while I was analyzing the bug. I post my notes anyway.

    I guess, it might be only #146189.

    The failing test is:

    # The same, but tack on a 1 bit so it rounds up to infinity.
    big = (1 << 25) - 1
    big = math.ldexp(big, 127 - 24)
    try:
        struct.pack(">f", big)
    except OverflowError:
        print("ok")
    else:
        print("error: expect OverflowError")

    Before, it raised OverflowError. Now, it does not raise any exception (and so the test fails).

    With the change, bigendian_table is updated to use native functions:

    (gdb) p bigendian_table[18]
    $7 = {format = 70 'F', size = 8, alignment = 0, unpack = 0x3ffe9b40a80 <nu_float_complex>, 
      pack = 0x3ffe9b40758 <np_float_complex>}
    

    So struct.pack(">f", big) calls np_float() which does:

        float x = (float)PyFloat_AsDouble(v);
        if (x == -1 && PyErr_Occurred()) {
            PyErr_SetString(state->StructError,
                            "required argument is not a float");
            return -1;
        }
        memcpy(p, &x, sizeof x);
        return 0;

    Before, bp_float() was called, it does:

        double x = PyFloat_AsDouble(v);
        if (x == -1 && PyErr_Occurred()) {
            PyErr_SetString(state->StructError,
                            "required argument is not a float");
            return -1;
        }
        return PyFloat_Pack4(x, p, 0);
  24. vstinner commented on Mar 25, 2026

    @vstinner
    Member

    @skirpichev: What do you think of this fix? We might have to extend test_struct to test more cases (test the 3 endian: big, little, native).

    diff --git a/Modules/_struct.c b/Modules/_struct.c
    index d6995895c2b..c235e27a415 100644
    --- a/Modules/_struct.c
    +++ b/Modules/_struct.c
    @@ -763,14 +763,13 @@ np_halffloat(_structmodulestate *state, char *p, PyObject *v, const formatdef *f
     static int
     np_float(_structmodulestate *state, char *p, PyObject *v, const formatdef *f)
     {
    -    float x = (float)PyFloat_AsDouble(v);
    +    double x = PyFloat_AsDouble(v);
         if (x == -1 && PyErr_Occurred()) {
             PyErr_SetString(state->StructError,
                             "required argument is not a float");
             return -1;
         }
    -    memcpy(p, &x, sizeof x);
    -    return 0;
    +    return PyFloat_Pack4(x, p, PY_LITTLE_ENDIAN);
     }
     
     static int
  25. skirpichev commented on Mar 25, 2026

    @skirpichev
    MemberAuthor

    What do you think of this fix?

    Looks good. Please make a pr if you have a free time.

    This will also fix discrepancy in packing of nans on some platforms.

    We might have to extend test_struct to test more cases (test the 3 endian: big, little, native).

    Lets test also half-floats here. Just in case, they might be affected in principle.

  26. added 2 commits that reference this issue on Mar 25, 2026
  27. vstinner commented on Mar 25, 2026

    @vstinner
    Member

    I wrote #146422 to fix the issue and add more tests.

  28. added a commit that references this issue on Mar 26, 2026
  29. added a commit that references this issue on Mar 26, 2026
  30. added 2 commits that reference this issue on Mar 26, 2026
  31. removed their assignment
    on Mar 27, 2026
  32. encukou commented on Apr 16, 2026

    @encukou
    Member

    The PR added this to PyFloat_Pack8:

    CPython implementation detail: This function always succeeds in CPython.

    I think this is unfortunate, as it's a promise for future versions of CPython -- it essentially means CPython will never again be portable to non-IEEE-double platforms. That's a much stronger promise that the current version of Python requiring IEEE floating point.

    IMO, this should be an internal implementation detail.

  33. vstinner commented on Apr 16, 2026

    @vstinner
    Member

    it essentially means CPython will never again be portable to non-IEEE-double platforms

    Require IEEE 754 floating point numbers was a deliberate choice made in Python 3.11 to simplify the Python code (drop legacy code for non-IEEE platforms). New platforms are compatible with IEEE 754 which is a de facto standard. Only very old unmaintained plaforms don't support IEEE 754.

  34. added 3 commits that reference this issue on Apr 16, 2026
  35. encukou commented on Apr 17, 2026

    @encukou
    Member

    I agree with dropping untested code, but I don't think we should prevent porting CPython to old (or future) non-IEEE platforms.

  36. skirpichev commented on Apr 17, 2026

    @skirpichev
    MemberAuthor

    The only way to do this is to revert choice made. Thread: https://mail.python.org/archives/list/python-dev@python.org/thread/J5FSP6J4EITPY5C2UJI7HSL2GQCTCUWN/

    I would say it will be a step backward.

  37. added 5 commits that reference this issue on Apr 25, 2026
  38. skirpichev commented on May 4, 2026

    @skirpichev
    MemberAuthor

    https://discuss.python.org/t/deprecate-float-getformat/107151 - discussion of __getformat__ removal.

    Lets close this. If someone wish to continue - it would be better to open a new issue.

  39. encukou commented on Jun 15, 2026

    @encukou
    Member

    I opened one in the C API WG repo: capi-workgroup/decisions#107

  40. added a commit that references this issue on Jul 10, 2026
  41. added a commit that references this issue on Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.15bugs and security fixesbuildThe build process and cross-buildinterpreter-core(Objects, Python, Grammar, and Parser dirs)topic-C-API

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions