Skip to content

Py_MIN(), Py_MAX() and Py_ABS() cause C compatibility regressions #158942

Description

@lpyu001

Bug report

Summary

On GCC and clang, when compiling C , Py_MIN(), Py_MAX() and Py_ABS() are now GNU statement expressions that copy each argument into local variables named _x and _y. Code that used these public macros in an integer constant expression no longer compiles: file-scope array sizes, struct member array sizes, enum values and case labels all fail. Code that passes a bit-field also no longer compiles. When the caller passes a variable named _x or _y, the code compiles and the result is silently wrong: Py_MAX(a, _x) returns a, and Py_MIN(b, _y) and Py_ABS(_x) read an uninitialized local. Nested calls such as Py_MAX(Py_MIN(a, b), c) now emit -Wshadow warnings, and mixed-sign arguments such as Py_MIN(size_t_var, INT_MAX) emit -Wsign-compare warnings under GCC.

Reproduction Code

Requires GCC or clang compiling C. MSVC and C++ use the previous ternary definitions. The commands below use GCC 13.3.0 and clang 18.1.3 on x86-64 Linux, with -I pointing at a CPython source tree that includes the new Include/pymacro.h.

Wrong results:

/* wrong_result.c */
#include <Python.h>
#include <stdio.h>

int main(void)
{
    int a = 1, _x = 5;
    printf("Py_MAX(a, _x) = %d (expected 5)\n", Py_MAX(a, _x));

    int b = 9, _y = 2;
    printf("Py_MIN(b, _y) = %d (expected 2)\n", Py_MIN(b, _y));

    int c = -7;
    {
        int _x = c;
        printf("Py_ABS(_x) = %d (expected 7)\n", Py_ABS(_x));
    }
    return 0;
}
gcc -std=c11 -O0 -I Include -I . wrong_result.c -o t && ./t
gcc -std=c11 -O2 -I Include -I . wrong_result.c -o t && ./t
clang -std=c11 -O0 -I Include -I . wrong_result.c -o t && ./t

Compile failures and new warnings (each block is a separate file):

/* file_scope.c: compile with -c */
#include <Python.h>

static char buf[Py_MAX(sizeof(long), 16)];
struct S { char data[Py_MIN(8, 32)]; };
enum { E = Py_MAX(3, 7) };
/* bitfield.c: compile with -c */
#include <Python.h>

struct S { unsigned bf : 4; };
int f(struct S *s, int k) { return Py_MAX(s->bf, k); }
/* case_label.c: compile with -c */
#include <Python.h>

int f(int v)
{
    switch (v) {
    case Py_MAX(1, 2): return 1;
    default: return 0;
    }
}
/* shadow.c: compile with -c -Wall -Wextra -Wshadow */
#include <Python.h>

int f(int a, int b, int c) { return Py_MAX(Py_MIN(a, b), c); }
/* signcmp.c: compile with -c -Wall -Wextra */
#include <Python.h>
#include <limits.h>

int f(size_t n) { return (int)Py_MIN(n, INT_MAX); }

Actual Behavior

wrong_result.c output:

--- gcc -O0
Py_MAX(a, _x) = 1 (expected 5)
Py_MIN(b, _y) = -219883104 (expected 2)
Py_ABS(_x) = 32766 (expected 7)
--- gcc -O2
Py_MAX(a, _x) = 1 (expected 5)
Py_MIN(b, _y) = 0 (expected 2)
Py_ABS(_x) = 0 (expected 7)
--- clang -O0
Py_MAX(a, _x) = 1 (expected 5)
Py_MIN(b, _y) = 0 (expected 2)
Py_ABS(_x) = 0 (expected 7)

GCC compiles wrong_result.c without any warning at the default warning level. Under -Wall, clang warns variable '_y' is uninitialized when used within its own initialization [-Wuninitialized] and the same for _x. GCC gives no such warning.

Compile failures with GCC:

=== gcc -std=c11 -c file_scope.c
Include/pymacro.h:127:8: error: braced-group within expression allowed only inside a function
Include/pymacro.h:121:8: error: braced-group within expression allowed only inside a function
Include/pymacro.h:127:8: error: braced-group within expression allowed only inside a function
=== gcc -std=c11 -c bitfield.c
bitfield.c:4:43: error: ‘typeof’ applied to a bit-field
=== gcc -std=c11 -c case_label.c
case_label.c:6:5: error: case label does not reduce to an integer constant

Compile failures with clang:

=== clang -std=c11 -c file_scope.c
file_scope.c:3:17: error: statement expression not allowed at file scope
file_scope.c:4:22: error: statement expression not allowed at file scope
file_scope.c:5:12: error: statement expression not allowed at file scope
=== clang -std=c11 -c bitfield.c
bitfield.c:4:36: error: invalid application of 'typeof' to bit-field
=== clang -std=c11 -c case_label.c
case_label.c:6:10: error: expression is not an integer constant expression

New warnings:

=== gcc -std=c11 -Wall -Wextra -Wshadow -c shadow.c
Include/pymacro.h:121:26: warning: declaration of ‘_x’ shadows a previous local [-Wshadow]
=== gcc -std=c11 -Wall -Wextra -Wshadow -c signcmp.c
Include/pymacro.h:123:14: warning: comparison of integer expressions of different signedness: ‘size_t’ {aka ‘long unsigned int’} and ‘int’ [-Wsign-compare]
Include/pymacro.h:123:26: warning: operand of ‘?:’ changes signedness from ‘int’ to ‘size_t’ {aka ‘long unsigned int’} due to unsignedness of other operand [-Wsign-compare]
=== clang -std=c11 -Wall -Wextra -Wshadow -c shadow.c
shadow.c:3:44: warning: declaration shadows a local variable [-Wshadow]

clang emits no -Wsign-compare warning for signcmp.c.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Activity

added
type-bugAn unexpected behavior, bug, or error
on Oct 6, 2026

ZeroIntensity commented on Oct 7, 2026

@ZeroIntensity
Member

Has this caused problems in practice anywhere, or is this just conceptual? Some of these examples look very artificial (it is not particularly useful to use Py_MAX in switch cases, for example).

cc @vstinner

picnixz commented on Oct 7, 2026

@picnixz
Member

All the examples except the switch one look important to fix. The one with file_scope is however too fragile IMO as Py_MIN/MAX could be considered a function call (in which case it shouldn't be used in arrays static lengths).

picnixz commented on Oct 7, 2026

@picnixz
Member

IIRC @vstinner you recently altered those macros

added
3.16new features, bugs and security fixes
type-bugAn unexpected behavior, bug, or error
interpreter-core(Objects, Python, Grammar, and Parser dirs)
buildThe build process and cross-build
and removed
type-bugAn unexpected behavior, bug, or error
interpreter-core(Objects, Python, Grammar, and Parser dirs)
on Oct 7, 2026
added 2 commits that reference this issue on Oct 7, 2026

vstinner commented on Oct 7, 2026

@vstinner
Member

I created PR #158969 to fix most issues reported here. I didn't fix file_scope.c: you need a workaround in your code for this one. The case_label.c case is not fixed, but just: don't do that :-)

/* file_scope.c: compile with -c */
#include <Python.h>
static char buf[Py_MAX(sizeof(long), 16)];
struct S { char data[Py_MIN(8, 32)]; };
enum { E = Py_MAX(3, 7) };

It's unfortunate that this use case is broken by the new macros implementation. If you need the macros at file scope, you might have to bring your own implementation of these macros (copy Python 3.15 macros to your C file and rename them).

/* shadow.c: compile with -c -Wall -Wextra -Wshadow */
int f(int a, int b, int c) { return Py_MAX(Py_MIN(a, b), c); }

This one is fixed by my PR.

/* signcmp.c: compile with -c -Wall -Wextra */
int f(size_t n) { return (int)Py_MIN(n, INT_MAX); }

That's a legit bug in your bug. It's a nice and deliberate side effect of the new implementation: detect such sign comparison bug!

The correct size is: int f(size_t n) { return (int)Py_MIN(n, (size_t)INT_MAX); }.

/* case_label.c: compile with -c */
case Py_MAX(1, 2): return 1;

I don't think that's a realistic use case. Just don't do that :-) Did you use a LLM to generate these C files?

lpyu001 commented on Oct 8, 2026

@lpyu001
ContributorAuthor

Yes, I first noticed the issue where variables named _x and _y could be shadowed. I then used an LLM to further evaluate #157496 and generated these test cases.

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

    3.16new features, bugs and security fixesbuildThe build process and cross-buildinterpreter-core(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions