Repository navigation
struct (un)packing of half-precision nan floats is non-invertible #130317
Copy link
Copy link
Closed
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
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__])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
Reactions are currently unavailable
Metadata
Metadata
Assignees
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
- StatusShow more project fieldsDone

Activity
skirpichev commentedon Feb 20, 2025
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
datato one or another quiet nan:cpython/Objects/floatobject.c
Lines 2402 to 2405 in 12e1d30
The PyFloat_Pack2() also ignores all payload from double nan:
cpython/Objects/floatobject.c
Lines 2050 to 2059 in 12e1d30
Looks as a bug for me.
CC @mdickinson
Edit: assuming doubles are binary64, following patch fix your tests:
FYI: #55943. Probably the reason why payload was ignored is that the patch was adapted from numpy sources.
skirpichev commentedon Feb 20, 2025
@tim-one, does it looks as an issue for you?
pythongh-130317: fix PyFloat_Pack2/Unpack2 for NaN's with payload
pythongh-130317: fix PyFloat_Pack2/Unpack2 for NaN's with payload
skirpichev commentedon Feb 23, 2025
PR is ready for review: #130452
gh-130317: Fix PyFloat_Pack/Unpack[24] for NaN's with payload (#130452)
vstinner commentedon Apr 28, 2025
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.
vstinner commentedon Apr 29, 2025
I reopen the issue, there are failures on x86 (32-bit): #130452 (comment)
9 remaining items
vstinner commentedon Apr 30, 2025
Documentating the issue sounds like a good option.
I don't think that it's worth it it to add a new API just for sNaN.
pythongh-130317: fix test_pack_unpack_roundtrip() and add docs
skirpichev commentedon Apr 30, 2025
PR is ready: #133204
Sure. Though, maybe it's something we could keep in mind. Using
PyObject *for argument and as return value will solve this issue: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.
gh-130317: Fix test_pack_unpack_roundtrip() and add docs (#133204)
vstinner commentedon May 1, 2025
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().Fix SNaN broken tests on HP PA RISC
gh-130317: Fix SNaN broken tests on HP PA RISC (#140452)
pythongh-130317: Fix SNaN broken tests on HP PA RISC (pythonGH-140452)
[3.14] gh-130317: Fix SNaN broken tests on HP PA RISC (GH-140452) (#1…
pythongh-130317: Fix SNaN broken tests on HP PA RISC (python#140452)