GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — struct (un)packing of half-precision `nan` floats is non-invertible · Issue #130317 · python/cpython

struct (un)packing of half-precision nan floats is non-invertible #130317

New issue
Copy link
New issue
Copy link
Closed
Closed
struct (un)packing of half-precision nan floats is non-invertible#130317
Copy link
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@seh-dev
seh-dev
opened on Feb 19, 2025
Issue body actions

Bug report

Bug description:

I noticed that chaining struct.unpack() and struct.pack() for IEEE 754 Half Precision floats (e) is non-invertible for nan. E.g.:

import struct

original_bytes = b'\xff\xff'

unpacked_float = struct.unpack('e', original_bytes)[0]  # nan
repacked_bytes = struct.pack('e', unpacked_float)  # b'\x00\xfe'  != b'\xff\xff'

IEEE nans aren't unique, so this isn't that surprising... However I found it curious that the same behavior is not exhibited for float (f) or double (d) format, where every original bit pattern I tested could be recovered from the unpacked nan object.

Is this by design?

Here's a quick pytest script that tests over a broad range of nan/inf/-inf cases for each encoding format.

# /// script
# requires-python = ">=3.11"
# dependencies = ["pytest"]
# ///
import struct
import pytest


# Floating Point Encodings Based on IEEE 754 per https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats
# binary 16 (half precision) - 1 bit sign, 5 bit exponent, 11 bit significand
# binary 32 (single precision) - 1 bit sign, 8 bit exponent, 23 bit significand
# binary 64 (double precision) - 1 bit sign, 11 bit exponent, 52 bit significand


MAX_TEST_CASES = 100000  # limit number of bit patterns being sampled so we aren't waiting too long


@pytest.mark.parametrize(["precision_format", "precision", "exponent_bits"], [("f", 32, 8), ("d", 64, 11), ("e", 16, 5)])
@pytest.mark.parametrize("sign_bit", [0, 1])
@pytest.mark.parametrize("endianness", ["little", "big"])
def test_struct_floats(precision_format: str, precision: int, exponent_bits: int, sign_bit: int, endianness: str):
    significand_bits = precision - exponent_bits - 1

    n_tests = min(MAX_TEST_CASES, 2**significand_bits)

    significand_patterns = [significand_bits * "0", significand_bits * "1"] + [
        bin(i + 1)[2:] for i in range(1, 2**significand_bits, 2**significand_bits // n_tests)
    ]

    for i in range(n_tests):
        binary = str(sign_bit) + "1" * exponent_bits + significand_patterns[i]
        if endianness == "big":
            format = ">" + precision_format
        elif endianness == "little":
            format = "<" + precision_format
        else:
            raise NotImplementedError()

        test_bytes = int(binary, base=2).to_bytes(precision // 8, endianness)

        unpacked = struct.unpack(format, test_bytes)
        assert len(unpacked) == 1

        repacked = struct.pack(format, unpacked[0])

        assert (
            repacked == test_bytes
        ), f"struct pack/unpack was not invertible for format {format} with raw value: {test_bytes} -> unpacks to {unpacked[0]}, repacks to {repacked}"

if __name__ == "__main__":
    pytest.main([__file__])

Image

CPython versions tested on:

3.13, 3.11, 3.12

Operating systems tested on:

Linux, Windows

Linked PRs

  • gh-130317: fix PyFloat_Pack/Unpack[24] for NaN's with payload #130452
  • gh-130317: Fix strict aliasing in PyFloat_Pack8() #133150
  • gh-130317: Skip test_pack_unpack_roundtrip_for_nans() on x86 #133155
  • gh-130317: fix test_pack_unpack_roundtrip() and add docs #133204
  • gh-130317: Fix SNaN broken tests on HP PA RISC #140452
  • [3.14] gh-130317: Fix SNaN broken tests on HP PA RISC (GH-140452) #140467

Activity

Next
  1. seh-dev
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Feb 19, 2025
  2. skirpichev
    added
    extension-modulesC modules in the Modules dirC modules in the Modules dir
    on Feb 20, 2025
  3. skirpichev
    self-assigned this
    on Feb 20, 2025
  4. skirpichev commented on Feb 20, 2025

    @skirpichev
    skirpichev
    on Feb 20, 2025
    Member
    More actions

    It seems you are on IEEE-platform, or unpacking special values will fail for float and double formats. So for those formats, pack/unpack functions work by copying bits.

    But not PyFloat_Pack2() and PyFloat_Unpack2(). E.g. the later just ignores all payload in the nan value and maps data to one or another quiet nan:

    cpython/Objects/floatobject.c

    Lines 2402 to 2405 in 12e1d30

    else {
    /* NaN */
    return sign ? -fabs(Py_NAN) : fabs(Py_NAN);
    }

    The PyFloat_Pack2() also ignores all payload from double nan:

    cpython/Objects/floatobject.c

    Lines 2050 to 2059 in 12e1d30

    else if (isnan(x)) {
    /* There are 2046 distinct half-precision NaNs (1022 signaling and
    1024 quiet), but there are only two quiet NaNs that don't arise by
    quieting a signaling NaN; we get those by setting the topmost bit
    of the fraction field and clearing all other fraction bits. We
    choose the one with the appropriate sign. */
    sign = (copysign(1.0, x) == -1.0);
    e = 0x1f;
    bits = 512;
    }

    Is this by design?

    Looks as a bug for me.

    CC @mdickinson

    Edit: assuming doubles are binary64, following patch fix your tests:

    diff --git a/Objects/floatobject.c b/Objects/floatobject.c
    index 3b72a1e7c3..e473fb72fe 100644
    --- a/Objects/floatobject.c
    +++ b/Objects/floatobject.c
    @@ -2048,14 +2048,16 @@ PyFloat_Pack2(double x, char *data, int le)
             bits = 0;
         }
         else if (isnan(x)) {
    -        /* There are 2046 distinct half-precision NaNs (1022 signaling and
    -           1024 quiet), but there are only two quiet NaNs that don't arise by
    -           quieting a signaling NaN; we get those by setting the topmost bit
    -           of the fraction field and clearing all other fraction bits. We
    -           choose the one with the appropriate sign. */
             sign = (copysign(1.0, x) == -1.0);
             e = 0x1f;
    -        bits = 512;
    +
    +        uint64_t v;
    +
    +        memcpy(&v, &x, sizeof(v));
    +        bits = v & 0x1ff;
    +        if (v & 0x800000000000) {
    +            bits += 0x200;
    +        }
         }
         else {
             sign = (x < 0.0);
    @@ -2401,7 +2403,16 @@ PyFloat_Unpack2(const char *data, int le)
             }
             else {
                 /* NaN */
    -            return sign ? -fabs(Py_NAN) : fabs(Py_NAN);
    +            uint64_t v = ((sign? 0xff00000000000000 : 0x7f00000000000000)
    +                          + 0xf0000000000000);
    +
    +            if (f & 0x200) {
    +                v += 0x800000000000;
    +                f -= 0x200;
    +            }
    +            v += f;
    +            memcpy(&x, &v, sizeof(v));
    +            return x;
             }
         }
     

    FYI: #55943. Probably the reason why payload was ignored is that the patch was adapted from numpy sources.

  5. skirpichev commented on Feb 20, 2025

    @skirpichev
    skirpichev
    on Feb 20, 2025
    Member
    More actions

    @tim-one, does it looks as an issue for you?

  6. skirpichev
    added a commit that references this issue on Feb 22, 2025

    pythongh-130317: fix PyFloat_Pack2/Unpack2 for NaN's with payload

    5fc8222
  7. skirpichev
    removed their assignment
    on Feb 22, 2025
  8. skirpichev
    added a commit that references this issue on Feb 23, 2025

    pythongh-130317: fix PyFloat_Pack2/Unpack2 for NaN's with payload

    971fd98
  9. skirpichev commented on Feb 23, 2025

    @skirpichev
    skirpichev
    on Feb 23, 2025
    Member
    More actions

    PR is ready for review: #130452

  10. vstinner
    added a commit that references this issue on Apr 28, 2025

    gh-130317: Fix PyFloat_Pack/Unpack[24] for NaN's with payload (#130452)

    6157135
  11. vstinner commented on Apr 28, 2025

    @vstinner
    vstinner
    on Apr 28, 2025
    Member
    More actions

    Fixed in the main branch (future Python 3.14) by change 6157135. The change is not backported to 3.13 branch since it's a minor issue.

  12. vstinner
    closed this as completedon Apr 28, 2025
  13. vstinner commented on Apr 29, 2025

    @vstinner
    vstinner
    on Apr 29, 2025
    Member
    More actions

    I reopen the issue, there are failures on x86 (32-bit): #130452 (comment)

  14. vstinner
    reopened this on Apr 29, 2025
  15. skirpichev
    self-assigned this
    on Apr 29, 2025
  16. 9 remaining items

  17. vstinner commented on Apr 30, 2025

    @vstinner
    vstinner
    on Apr 30, 2025
    Member
    More actions

    Maybe it worth documenting?

    Documentating the issue sounds like a good option.

    (Passing by reference - works.)

    I don't think that it's worth it it to add a new API just for sNaN.

  18. skirpichev
    added a commit that references this issue on Apr 30, 2025

    pythongh-130317: fix test_pack_unpack_roundtrip() and add docs

    b474ecf
  19. skirpichev commented on Apr 30, 2025

    @skirpichev
    skirpichev
    on Apr 30, 2025
    Member
    More actions

    Documenting the issue sounds like a good option.

    PR is ready: #133204

    I don't think that it's worth it it to add a new API just for sNaN.

    Sure. Though, maybe it's something we could keep in mind. Using PyObject * for argument and as return value will solve this issue:

    unsigned char* PyFloat_Pack(PyObject *x, size_t size, int le);
    PyObject* PyFloat_Unpack(const unsigned char *p, size_t size, int le);

    Two functions vs 6, we also can support someday IEEE sizes>=128.

    Currently, we can workaround this problem in struct.pack/unpack(), at cost of code complexity. But I doubt it worth.

  20. vstinner
    added a commit that references this issue on May 1, 2025

    gh-130317: Fix test_pack_unpack_roundtrip() and add docs (#133204)

    ad2f088
  21. vstinner
    closed this as completedon May 1, 2025
  22. vstinner commented on May 1, 2025

    @vstinner
    vstinner
    on May 1, 2025
    Member
    More actions

    Currently, we can workaround this problem in struct.pack/unpack(), at cost of code complexity. But I doubt it worth.

    Most platforms are now 64-bit and don't seem to be affected by the issue. I don't think that it's worth it to invest time on fixing struct.pack/unpack(). And I would prefer that PyFloat_Pack/Unpack*() would remain consistent with struct.pack/unpack().

  23. skirpichev
    removed their assignment
    on May 2, 2025
  24. stefanor
    added a commit that references this issue on Oct 22, 2025

    Fix SNaN broken tests on HP PA RISC

    96815eb
  25. vstinner
    added a commit that references this issue on Oct 22, 2025

    gh-130317: Fix SNaN broken tests on HP PA RISC (#140452)

    76fea55
  26. miss-islington
    added a commit that references this issue on Oct 22, 2025

    pythongh-130317: Fix SNaN broken tests on HP PA RISC (pythonGH-140452)

    fe6f7fd
  27. vstinner
    added a commit that references this issue on Oct 22, 2025

    [3.14] gh-130317: Fix SNaN broken tests on HP PA RISC (GH-140452) (#1…

    853e5d9
  28. StanFromIreland
    added a commit that references this issue on Dec 6, 2025

    pythongh-130317: Fix SNaN broken tests on HP PA RISC (python#140452)

    f839c45
  29. serhiy-storchaka
    added this to Struct, memoryview and array issues 🏗️on Jul 13, 2026
  30. github-project-automation
    moved this to Todo in Struct, memoryview and array issues 🏗️on Jul 13, 2026
  31. github-project-automation
    moved this from Todo to Done in Struct, memoryview and array issues 🏗️on Jul 13, 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

    extension-modulesC modules in the Modules dirC modules in the Modules dirtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    • Struct, memoryview and array issues 🏗️
      Status
      Done
      Show more project fields

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.