GeezerVison is showing github.com — `Py_MIN()`, `Py_MAX()` and `Py_ABS()` cause C compatibility regressions · Issue #158942 · python/cpython
Py_MIN(), Py_MAX() and Py_ABS() cause C compatibility regressions #158942
Labels 3.16new features, bugs and security fixesnew features, bugs and security fixesbuildThe build process and cross-buildThe build process and cross-buildinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error DescriptionBug 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
ActivityZeroIntensity commented on 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_MAX in switch cases, for example).
cc @vstinner Reacted by Victor Stinner picnixz commented on 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 commented on Oct 7, 2026 IIRC @vstinner you recently altered those macros vstinner commented on 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 :-)
/* 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 ContributorAuthor More actions 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. Reacted by Victor Stinner
MetadataMetadata3.16new features, bugs and security fixesnew features, bugs and security fixesbuildThe build process and cross-buildThe build process and cross-buildinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error DevelopmentNo branches or pull requestsIssue actions
|