Skip to content

memoryview.cast("?") has undefined behavior on some platforms/compilers #148390

Closed

Description

@picnixz

Bug report

Bug description:

I am opening an issue to follow-up #83870 and https://discuss.python.org/t/behavior-of-struct-format-native-bool/3774 while I was investigating #148286. Here is the problem:

>>> import struct
>>> struct.pack("????", 0, 1, 2, 3)
b'\x00\x01\x01\x01'
>>> struct.unpack("????", b"\0\1\2\3")
(False, True, True, True)

For struct.[un]pack, we see that there is a conversion from bool(object) to \x00 or \x01 as well as from \x00 to False and anything else that is 1 byte to True. The docs say:

For the '?' format character, the return value is either True or False. When packing, the truth value of the argument object is used. Either 0 or 1 in the native or standard bool representation will be packed, and any non-zero value will be True when unpacking.

but as the DPO thread says, it may be confusing. I think it makes more sense to say that packing to ? (for Python) is more or less equivalent to cast it to bool and then to the C _Bool (in Python, there is a well-defined conversion from any object to our own bool type).

However, for memoryview, we have the following:

>>> memoryview(b'\0\1\2\3').cast("?").tolist()
[False, True, False, True]

In particular, there is a reduction modulo $2$ for memory view items instead. There is however an undefined behavior here because tolist() performs the following on each memoryview's item:

_Bool c;
memcpy((char *)&c, ptr, sizeof _Bool);

The above is an undefined behavior. I think memoryview.cast(fmt).tolist() should behave as if we were doing struct.unpack(fmt * len(mv), mv.obj) instead. If this is not desired, we should at least document this discrepancy. Unfortunately, PEP-3118 does not address this (AFAIK).

cc @encukou @vstinner @serhiy-storchaka @ngoldbaum

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Linked PRs

Activity

  1. vstinner commented on Apr 11, 2026

    @vstinner
    Member

    Previously, struct.unpack('?', data) had an undefined behavior: see issue gh-125118. It has been fixed by the commit 87d7315.

    I agree that we should fix memoryview() behavior to fix the undefined behavior.

  2. self-assigned this
    on Apr 11, 2026
  3. picnixz commented on Apr 11, 2026

    @picnixz
    MemberAuthor

    Should we consider it a real feature or a bug (i.e how to backport it?)? I don't know how annoying it can be for downstream users. It might hide subtle bugs, but I don't know how it's used in general.

  4. skirpichev commented on Apr 12, 2026

    @skirpichev
    Member

    I think this should be backported on same ground as for referenced issue. I don't think that people may rely on undefined behavior.

  5. added a commit that references this issue on Apr 12, 2026
  6. removed their assignment
    on Apr 12, 2026
  7. added a commit that references this issue on Apr 15, 2026
  8. vstinner commented on Apr 15, 2026

    @vstinner
    Member

    Hum, I fail to reproduce the memoryview behavior on Fedora Stable with GCC 15.2.1. On a release build (gcc -O3), I get:

    >>> memoryview(b'\0\1\2\3').cast("?").tolist()
    [False, True, True, True]

    I also get [False, True, True, True] on the Python executables provided by Fedora (I tested Python 3.11 to Python 3.15): they are also built with GCC.


    Ah! I'm able to reproduce the issue when building Python with Clang 21.1.8 on Fedora 43.

    >>> memoryview(b'\0\1\2\3').cast("?").tolist()
    [False, True, False, True]
  9. picnixz commented on Apr 15, 2026

    @picnixz
    MemberAuthor

    Yeah, @encukou told me it wasn't present on all compilers/platforms (which is why I reworded the NEWS entry).

  10. changed the title [-]`memoryview.cast("?")` has undocumented undefined behavior[/-] [+]`memoryview.cast("?")` has undefined behavior on some platforms/compilers[/+] on Apr 15, 2026
  11. vstinner commented on Apr 15, 2026

    @vstinner
    Member

    I'm also able to reproduce the issue on Python 3.3 built with Clang 21. (memoryview.cast() method was added to Python 3.3.)

    @picnixz:

    Should we consider it a real feature or a bug (i.e how to backport it?)? I don't know how annoying it can be for downstream users. It might hide subtle bugs, but I don't know how it's used in general.

    This question is tricky. Getting a different answer depending on the compiler is quite unusual and sounds more like a bug to me. I would suggest backporting the fix.

    If nobody reported the issue already, it's maybe because no one tried to use this feature before.

    If there are affected users, they should upgrade Python, or use a workaround like:

    >>> m=memoryview(b'\0\1\2\3').cast("?")
    >>> list(map(bool, memoryview(m).cast('B').tolist()))
    [False, True, True, True]

    There is at least FreeBSD which builds Python with clang.

  12. picnixz commented on Apr 15, 2026

    @picnixz
    MemberAuthor

    I also decided to backport this. This may hide nasty issues, and it's a real bug. I prefer breaking broken things rather than leaving them unnoticed.

  13. picnixz commented on Apr 15, 2026

    @picnixz
    MemberAuthor

    Note: I will do the backport next week as the automatic ones failed and I do not k oe if I will be available before.

  14. added 2 commits that reference this issue on Apr 17, 2026
  15. added a commit that references this issue on Apr 25, 2026
  16. added a commit that references this issue on May 1, 2026
  17. added a commit that references this issue on May 8, 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

    interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions