Skip to content

Commit 5b116fe

Browse files
anandoleecopybara-github
authored andcommitted
Breaking change: Raise errors in OSS when assign bool to int/enum field in Python Proto.
https://protobuf.dev/news/2025-09-19/#python-reject-bool-enum-int PiperOrigin-RevId: 843305319
1 parent 29c14f9 commit 5b116fe

File tree

7 files changed

+69
-137
lines changed

7 files changed

+69
-137
lines changed

python/convert.c

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,12 @@ PyObject* PyUpb_UpbToPy(upb_MessageValue val, const upb_FieldDef* f,
6161
}
6262
}
6363

64-
// TODO: raise error in 2026 Q1 release
65-
static void WarnBool(const upb_FieldDef* f) {
66-
static int bool_warning_count = 100;
67-
if (bool_warning_count > 0) {
68-
--bool_warning_count;
69-
PyErr_WarnFormat(PyExc_DeprecationWarning, 3,
70-
"Field %s: Expected an int, got a boolean. This "
71-
"will be rejected in 7.34.0, please fix it before that",
72-
upb_FieldDef_FullName(f));
73-
}
74-
}
75-
7664
static bool PyUpb_GetInt64(PyObject* obj, const upb_FieldDef* f, int64_t* val) {
7765
if (PyBool_Check(obj)) {
78-
WarnBool(f);
66+
PyErr_Format(PyExc_TypeError,
67+
"Field %s: Expected an int, got a boolean.",
68+
upb_FieldDef_FullName(f));
69+
return false;
7970
}
8071
// We require that the value is either an integer or has an __index__
8172
// conversion.
@@ -98,7 +89,10 @@ static bool PyUpb_GetInt64(PyObject* obj, const upb_FieldDef* f, int64_t* val) {
9889
static bool PyUpb_GetUint64(PyObject* obj, const upb_FieldDef* f,
9990
uint64_t* val) {
10091
if (PyBool_Check(obj)) {
101-
WarnBool(f);
92+
PyErr_Format(PyExc_TypeError,
93+
"Field %s: Expected an int, got a boolean.",
94+
upb_FieldDef_FullName(f));
95+
return false;
10296
}
10397
// We require that the value is either an integer or has an __index__
10498
// conversion.
@@ -199,7 +193,10 @@ static bool PyUpb_PyToUpbEnum(PyObject* obj, const upb_FieldDef* f,
199193
return true;
200194
} else {
201195
if (PyBool_Check(obj)) {
202-
WarnBool(f);
196+
PyErr_Format(PyExc_TypeError,
197+
"Field %s: Expected an int, got a boolean.",
198+
upb_FieldDef_FullName(f));
199+
return false;
203200
}
204201
int32_t i32;
205202
if (!PyUpb_GetInt32(obj, f, &i32)) return false;

python/google/protobuf/internal/message_test.py

Lines changed: 49 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,58 +1477,60 @@ def testMessageClassName(self, message_module):
14771477
'TestAllTypes.NestedMessage', nested.__class__.__qualname__
14781478
)
14791479

1480+
def create_bool_to_int(self, message_module, **kwargs):
1481+
with self.assertRaises(TypeError) as e:
1482+
m = message_module.TestAllTypes(**kwargs)
1483+
self.assertIn('bool', str(e.exception))
1484+
return None
1485+
1486+
def assign_bool_to_int(self, msg, field_name, value):
1487+
old_value = getattr(msg, field_name)
1488+
with self.assertRaises(TypeError) as e:
1489+
setattr(msg, field_name, value)
1490+
self.assertIn('bool', str(e.exception))
1491+
self.assertEqual(getattr(msg, field_name), old_value)
1492+
1493+
def assign_bool_to_map_or_extension(self, msg, field_name, key, value):
1494+
with self.assertRaises(TypeError) as e:
1495+
getattr(msg, field_name)[key] = value
1496+
self.assertIn('bool', str(e.exception))
1497+
14801498
def testAssignBoolToEnum(self, message_module):
1481-
# TODO: change warning into error in 2026 Q1
1482-
# with self.assertRaises(TypeError):
1483-
with warnings.catch_warnings(record=True) as w:
1484-
m = message_module.TestAllTypes(optional_nested_enum=True)
1485-
self.assertIn('bool', str(w[0].message))
1486-
self.assertEqual(m.optional_nested_enum, 1)
1499+
m = self.create_bool_to_int(message_module, optional_nested_enum=True)
1500+
if m is not None:
1501+
self.assertEqual(m.optional_nested_enum, 1)
14871502

14881503
m = message_module.TestAllTypes(optional_nested_enum=2)
1489-
with warnings.catch_warnings(record=True) as w:
1490-
m.optional_nested_enum = True
1491-
self.assertIn('bool', str(w[0].message))
1492-
self.assertEqual(m.optional_nested_enum, 1)
1504+
self.assign_bool_to_int(m, 'optional_nested_enum', True)
14931505

14941506
with warnings.catch_warnings(record=True) as w:
14951507
m.optional_nested_enum = 2
14961508
self.assertFalse(w)
14971509
self.assertEqual(m.optional_nested_enum, 2)
14981510

14991511
def testBoolToRepeatedEnum(self, message_module):
1500-
with warnings.catch_warnings(record=True) as w:
1501-
m = message_module.TestAllTypes(repeated_nested_enum=[True])
1502-
self.assertIn('bool', str(w[0].message))
1503-
self.assertEqual(m.repeated_nested_enum, [1])
1512+
m = self.create_bool_to_int(message_module, repeated_nested_enum=[True])
1513+
if m is not None:
1514+
self.assertEqual(m.repeated_nested_enum, [1])
15041515

15051516
m = message_module.TestAllTypes()
1506-
with warnings.catch_warnings(record=True) as w:
1507-
m.repeated_nested_enum.append(True)
1508-
self.assertIn('bool', str(w[0].message))
1509-
self.assertEqual(m.repeated_nested_enum, [1])
1517+
with self.assertRaises(TypeError) as e:
1518+
m = message_module.TestAllTypes(repeated_nested_enum=[True])
1519+
self.assertIn('bool', str(e.exception))
1520+
self.assertEqual(m.repeated_nested_enum, [])
15101521

15111522
def testBoolToOneofEnum(self, message_module):
15121523
m = unittest_pb2.TestOneof2()
1513-
with warnings.catch_warnings(record=True) as w:
1514-
m.foo_enum = True
1515-
self.assertIn('bool', str(w[0].message))
1516-
self.assertEqual(m.foo_enum, 1)
1524+
self.assign_bool_to_int(m, 'foo_enum', True)
15171525

15181526
def testBoolToMapEnum(self, message_module):
15191527
m = map_unittest_pb2.TestMap()
1520-
with warnings.catch_warnings(record=True) as w:
1521-
m.map_int32_enum[10] = True
1522-
self.assertIn('bool', str(w[0].message))
1523-
self.assertEqual(m.map_int32_enum[10], 1)
1528+
self.assign_bool_to_map_or_extension(m, 'map_int32_enum', 10, True)
15241529

15251530
def testBoolToExtensionEnum(self, message_module):
15261531
m = unittest_pb2.TestAllExtensions()
1527-
with warnings.catch_warnings(record=True) as w:
1528-
m.Extensions[unittest_pb2.optional_nested_enum_extension] = True
1529-
self.assertIn('bool', str(w[0].message))
1530-
self.assertEqual(
1531-
m.Extensions[unittest_pb2.optional_nested_enum_extension], 1
1532+
self.assign_bool_to_map_or_extension(
1533+
m, 'Extensions', unittest_pb2.optional_nested_enum_extension, True
15321534
)
15331535

15341536
def testClosedEnumExtension(self, message_module):
@@ -1547,59 +1549,43 @@ def testClosedEnumExtension(self, message_module):
15471549
)
15481550

15491551
def testAssignBoolToInt(self, message_module):
1550-
with warnings.catch_warnings(record=True) as w:
1551-
m = message_module.TestAllTypes(optional_int32=True)
1552-
self.assertIn('bool', str(w[0].message))
1553-
self.assertEqual(m.optional_int32, 1)
1552+
m = self.create_bool_to_int(message_module, optional_int32=True)
1553+
if m is not None:
1554+
self.assertEqual(m.optional_int32, 1)
15541555

15551556
m = message_module.TestAllTypes(optional_uint32=123)
1556-
with warnings.catch_warnings(record=True) as w:
1557-
m.optional_uint32 = True
1558-
self.assertIn('bool', str(w[0].message))
1559-
self.assertEqual(m.optional_uint32, 1)
1557+
self.assign_bool_to_int(m, 'optional_uint32', True)
15601558

15611559
with warnings.catch_warnings(record=True) as w:
15621560
m.optional_uint32 = 321
15631561
self.assertFalse(w)
15641562
self.assertEqual(m.optional_uint32, 321)
15651563

15661564
def testAssignBoolToRepeatedInt(self, message_module):
1567-
with warnings.catch_warnings(record=True) as w:
1568-
m = message_module.TestAllTypes(repeated_int64=[True])
1569-
self.assertIn('bool', str(w[0].message))
1570-
self.assertEqual(m.repeated_int64, [1])
1565+
m = self.create_bool_to_int(message_module, repeated_int64=[True])
1566+
if m is not None:
1567+
self.assertEqual(m.repeated_int64, [1])
15711568

15721569
m = message_module.TestAllTypes()
1573-
with warnings.catch_warnings(record=True) as w:
1570+
with self.assertRaises(TypeError) as e:
15741571
m.repeated_int64.append(True)
1575-
self.assertIn('bool', str(w[0].message))
1576-
self.assertEqual(m.repeated_int64, [1])
1572+
self.assertIn('bool', str(e.exception))
1573+
self.assertEqual(m.repeated_int64, [])
15771574

15781575
def testAssignBoolToOneofInt(self, message_module):
15791576
m = unittest_pb2.TestOneof2()
1580-
with warnings.catch_warnings(record=True) as w:
1581-
m.foo_int = True
1582-
self.assertIn('bool', str(w[0].message))
1583-
self.assertEqual(m.foo_int, 1)
1577+
self.assign_bool_to_int(m, 'foo_int', True)
15841578

15851579
def testAssignBoolToMapInt(self, message_module):
15861580
m = map_unittest_pb2.TestMap()
1587-
with warnings.catch_warnings(record=True) as w:
1588-
m.map_int32_int32[10] = True
1589-
self.assertIn('bool', str(w[0].message))
1590-
self.assertEqual(m.map_int32_int32[10], 1)
1591-
1592-
with warnings.catch_warnings(record=True) as w:
1593-
m.map_int32_int32[True] = 1
1594-
self.assertIn('bool', str(w[0].message))
1595-
self.assertEqual(m.map_int32_int32[1], 1)
1581+
self.assign_bool_to_map_or_extension(m, 'map_int32_int32', 10, True)
1582+
self.assign_bool_to_map_or_extension(m, 'map_int32_int32', True, 1)
15961583

15971584
def testAssignBoolToExtensionInt(self, message_module):
15981585
m = unittest_pb2.TestAllExtensions()
1599-
with warnings.catch_warnings(record=True) as w:
1600-
m.Extensions[unittest_pb2.optional_int32_extension] = True
1601-
self.assertIn('bool', str(w[0].message))
1602-
self.assertEqual(m.Extensions[unittest_pb2.optional_int32_extension], 1)
1586+
self.assign_bool_to_map_or_extension(
1587+
m, 'Extensions', unittest_pb2.optional_int32_extension, True
1588+
)
16031589

16041590

16051591
@testing_refleaks.TestCase

python/google/protobuf/internal/type_checkers.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@
3232
from google.protobuf.internal import wire_format
3333

3434
_FieldDescriptor = descriptor.FieldDescriptor
35-
# TODO: Remove this warning count after 34.0
36-
# Assign bool to int/enum warnings will print 100 times at most which should
37-
# be enough for users to notice and do not cause timeout.
38-
_BoolWarningCount = 100
3935

4036
def TruncateToFourByteFloat(original):
4137
return struct.unpack('<f', struct.pack('<f', original))[0]
@@ -145,20 +141,15 @@ class IntValueChecker(object):
145141
"""Checker used for integer fields. Performs type-check and range check."""
146142

147143
def CheckValue(self, proposed_value):
148-
global _BoolWarningCount
149-
if type(proposed_value) == bool and _BoolWarningCount > 0:
150-
_BoolWarningCount -= 1
144+
if type(proposed_value) == bool:
151145
message = (
152-
'%.1024r has type %s, but expected one of: %s. This warning '
153-
'will turn into error in 7.34.0, please fix it before that.'
146+
'%.1024r has type %s, but expected one of: (int).'
154147
% (
155148
proposed_value,
156149
type(proposed_value),
157-
(int,),
158150
)
159151
)
160-
# TODO: Raise errors in 2026 Q1 release
161-
warnings.warn(message)
152+
raise TypeError(message)
162153

163154
if not hasattr(proposed_value, '__index__') or (
164155
type(proposed_value).__module__ == 'numpy' and
@@ -186,20 +177,16 @@ def __init__(self, enum_type):
186177
self._enum_type = enum_type
187178

188179
def CheckValue(self, proposed_value):
189-
global _BoolWarningCount
190-
if type(proposed_value) == bool and _BoolWarningCount > 0:
191-
_BoolWarningCount -= 1
180+
if type(proposed_value) == bool:
192181
message = (
193-
'%.1024r has type %s, but expected one of: %s. This warning '
194-
'will turn into error in 7.34.0, please fix it before that.'
182+
'%.1024r has type %s, but expected one of: (int).'
195183
% (
196184
proposed_value,
197185
type(proposed_value),
198-
(int,),
199186
)
200187
)
201-
# TODO: Raise errors in 2026 Q1 release
202-
warnings.warn(message)
188+
raise TypeError(message)
189+
203190
if not isinstance(proposed_value, numbers.Integral):
204191
message = ('%.1024r has type %s, but expected one of: %s' %
205192
(proposed_value, type(proposed_value), (int,)))

python/google/protobuf/pyext/map_container.cc

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,25 +108,21 @@ static bool PythonToMapKey(MapContainer* self, PyObject* obj, MapKey* key,
108108
switch (field_descriptor->cpp_type()) {
109109
case FieldDescriptor::CPPTYPE_INT32: {
110110
PROTOBUF_CHECK_GET_INT32(obj, value, false);
111-
CheckIntegerWithBool(obj, field_descriptor);
112111
key->SetInt32Value(value);
113112
break;
114113
}
115114
case FieldDescriptor::CPPTYPE_INT64: {
116115
PROTOBUF_CHECK_GET_INT64(obj, value, false);
117-
CheckIntegerWithBool(obj, field_descriptor);
118116
key->SetInt64Value(value);
119117
break;
120118
}
121119
case FieldDescriptor::CPPTYPE_UINT32: {
122120
PROTOBUF_CHECK_GET_UINT32(obj, value, false);
123-
CheckIntegerWithBool(obj, field_descriptor);
124121
key->SetUInt32Value(value);
125122
break;
126123
}
127124
case FieldDescriptor::CPPTYPE_UINT64: {
128125
PROTOBUF_CHECK_GET_UINT64(obj, value, false);
129-
CheckIntegerWithBool(obj, field_descriptor);
130126
key->SetUInt64Value(value);
131127
break;
132128
}
@@ -214,25 +210,21 @@ static bool PythonToMapValueRef(MapContainer* self, PyObject* obj,
214210
switch (field_descriptor->cpp_type()) {
215211
case FieldDescriptor::CPPTYPE_INT32: {
216212
PROTOBUF_CHECK_GET_INT32(obj, value, false);
217-
CheckIntegerWithBool(obj, field_descriptor);
218213
value_ref->SetInt32Value(value);
219214
return true;
220215
}
221216
case FieldDescriptor::CPPTYPE_INT64: {
222217
PROTOBUF_CHECK_GET_INT64(obj, value, false);
223-
CheckIntegerWithBool(obj, field_descriptor);
224218
value_ref->SetInt64Value(value);
225219
return true;
226220
}
227221
case FieldDescriptor::CPPTYPE_UINT32: {
228222
PROTOBUF_CHECK_GET_UINT32(obj, value, false);
229-
CheckIntegerWithBool(obj, field_descriptor);
230223
value_ref->SetUInt32Value(value);
231224
return true;
232225
}
233226
case FieldDescriptor::CPPTYPE_UINT64: {
234227
PROTOBUF_CHECK_GET_UINT64(obj, value, false);
235-
CheckIntegerWithBool(obj, field_descriptor);
236228
value_ref->SetUInt64Value(value);
237229
return true;
238230
}
@@ -261,7 +253,6 @@ static bool PythonToMapValueRef(MapContainer* self, PyObject* obj,
261253
}
262254
case FieldDescriptor::CPPTYPE_ENUM: {
263255
PROTOBUF_CHECK_GET_INT32(obj, value, false);
264-
CheckIntegerWithBool(obj, field_descriptor);
265256
if (allow_unknown_enum_values) {
266257
value_ref->SetEnumValue(value);
267258
return true;

python/google/protobuf/pyext/message.cc

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ bool CheckAndGetInteger(PyObject* arg, T* value) {
515515
// This definition includes everything with a valid __index__() implementation
516516
// and shouldn't cast the net too wide.
517517
if (!strcmp(Py_TYPE(arg)->tp_name, "numpy.ndarray") ||
518+
(!strcmp(Py_TYPE(arg)->tp_name, "bool")) ||
518519
ABSL_PREDICT_FALSE(!PyIndex_Check(arg))) {
519520
FormatTypeError(arg, "int");
520521
return false;
@@ -599,18 +600,6 @@ bool CheckAndGetBool(PyObject* arg, bool* value) {
599600
return true;
600601
}
601602

602-
void CheckIntegerWithBool(PyObject* arg, const FieldDescriptor* field_des) {
603-
static int bool_warning_count = 100;
604-
if (bool_warning_count > 0 && (!strcmp(Py_TYPE(arg)->tp_name, "bool"))) {
605-
--bool_warning_count;
606-
std::string error_msg =
607-
absl::StrCat(field_des->full_name(),
608-
": Expected an int, got a boolean. This "
609-
"will be rejected in 7.34.0, please fix it before that");
610-
PyErr_WarnEx(PyExc_DeprecationWarning, error_msg.c_str(), 3);
611-
}
612-
}
613-
614603
// Checks whether the given object (which must be "bytes" or "unicode") contains
615604
// valid UTF-8.
616605
bool IsValidUTF8(PyObject* obj) {
@@ -2333,25 +2322,21 @@ int InternalSetNonOneofScalar(Message* message,
23332322
switch (field_descriptor->cpp_type()) {
23342323
case FieldDescriptor::CPPTYPE_INT32: {
23352324
PROTOBUF_CHECK_GET_INT32(arg, value, -1);
2336-
CheckIntegerWithBool(arg, field_descriptor);
23372325
reflection->SetInt32(message, field_descriptor, value);
23382326
break;
23392327
}
23402328
case FieldDescriptor::CPPTYPE_INT64: {
23412329
PROTOBUF_CHECK_GET_INT64(arg, value, -1);
2342-
CheckIntegerWithBool(arg, field_descriptor);
23432330
reflection->SetInt64(message, field_descriptor, value);
23442331
break;
23452332
}
23462333
case FieldDescriptor::CPPTYPE_UINT32: {
23472334
PROTOBUF_CHECK_GET_UINT32(arg, value, -1);
2348-
CheckIntegerWithBool(arg, field_descriptor);
23492335
reflection->SetUInt32(message, field_descriptor, value);
23502336
break;
23512337
}
23522338
case FieldDescriptor::CPPTYPE_UINT64: {
23532339
PROTOBUF_CHECK_GET_UINT64(arg, value, -1);
2354-
CheckIntegerWithBool(arg, field_descriptor);
23552340
reflection->SetUInt64(message, field_descriptor, value);
23562341
break;
23572342
}
@@ -2379,7 +2364,6 @@ int InternalSetNonOneofScalar(Message* message,
23792364
}
23802365
case FieldDescriptor::CPPTYPE_ENUM: {
23812366
PROTOBUF_CHECK_GET_INT32(arg, value, -1);
2382-
CheckIntegerWithBool(arg, field_descriptor);
23832367
if (!field_descriptor->legacy_enum_field_treated_as_closed()) {
23842368
reflection->SetEnumValue(message, field_descriptor, value);
23852369
} else {

0 commit comments

Comments
 (0)