Skip to content

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

Description

@seh-dev

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.:

importstructoriginal_bytes=b'\xff\xff'unpacked_float=struct.unpack('e', original_bytes)[0] # nanrepacked_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"]# ///importstructimportpytest# 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 significandMAX_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"])deftest_struct_floats(precision_format: str, precision: int, exponent_bits: int, sign_bit: int, endianness: str):
significand_bits=precision-exponent_bits-1n_tests=min(MAX_TEST_CASES, 2**significand_bits)
significand_patterns= [significand_bits*"0", significand_bits*"1"] + [
bin(i+1)[2:] foriinrange(1, 2**significand_bits, 2**significand_bits//n_tests)
]
foriinrange(n_tests):
binary=str(sign_bit) +"1"*exponent_bits+significand_patterns[i]
ifendianness=="big":
format=">"+precision_formatelifendianness=="little":
format="<"+precision_formatelse:
raiseNotImplementedError()
test_bytes=int(binary, base=2).to_bytes(precision//8, endianness)
unpacked=struct.unpack(format, test_bytes)
assertlen(unpacked) ==1repacked=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

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions