Repository navigation
Py_MIN(), Py_MAX() and Py_ABS() cause C compatibility regressions #158942
Description
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 && ./tCompile 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
Activity
ZeroIntensity commentedon Oct 7, 2026
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_MAXinswitchcases, for example).cc @vstinner
picnixz commentedon Oct 7, 2026
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 commentedon Oct 7, 2026
IIRC @vstinner you recently altered those macros
pythongh-158942: Use more unique variable names in Py_MIN/MAX/ABS
pythongh-158942: Use more unique variable names in Py_MIN/MAX/ABS()
vstinner commentedon Oct 7, 2026
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 :-)
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).
This one is fixed by my PR.
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); }.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 commentedon Oct 8, 2026
Yes, I first noticed the issue where variables named
_xand_ycould be shadowed. I then used an LLM to further evaluate #157496 and generated these test cases.gh-158942: Use more unique variable names in Py_MIN/MAX/ABS() (#158969)