Repository navigation
struct (un)packing of half-precision nan floats is non-invertible #130317
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Feb 19, 2025 - addedextension-modulesC modules in the Modules dirC modules in the Modules dir
on 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:
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:
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.
@tim-one, does it looks as an issue for you?
PR is ready for review: #130452
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.
I reopen the issue, there are failures on x86 (32-bit): #130452 (comment)
9 remaining items
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.
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.
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().- added a commit that references this issue
on Oct 22, 2025 - added a commit that references this issue
on Oct 22, 2025 - moved this to Todo in Struct, memoryview and array issues 🏗️
on Jul 13, 2026 - moved this from Todo to Done in Struct, memoryview and array issues 🏗️
on Jul 13, 2026
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Bug report
Bug description:
I noticed that chaining
struct.unpack()andstruct.pack()for IEEE 754 Half Precision floats (e) is non-invertible fornan. E.g.:IEEE
nans aren't unique, so this isn't that surprising... However I found it curious that the same behavior is not exhibited forfloat(f) ordouble(d) format, where every original bit pattern I tested could be recovered from the unpackednanobject.Is this by design?
Here's a quick
pytestscript that tests over a broad range ofnan/inf/-infcases for each encoding format.CPython versions tested on:
3.13, 3.11, 3.12
Operating systems tested on:
Linux, Windows
Linked PRs