From f548a5a164d08b232570ed3fbbdcc30eae157a70 Mon Sep 17 00:00:00 2001 From: Carsten Piepel Date: Fri, 23 Oct 2026 08:11:29 -0400 Subject: [PATCH 1/2] docs(helpers): note that whole numbers in a data Part come back as floats google.protobuf.Value stores every number as a double, so an integer placed in a data Part is read back as a float on every transport. Document this on new_data_part and get_data_parts, add a bullet to the v1.0 migration guide with the client-side workaround, and pin the behavior with a test. Fixes #1328 Co-Authored-By: Claude Fable 5.1 --- docs/migrations/v1_0/README.md | 1 + src/a2a/helpers/proto_helpers.py | 10 ++++++++++ tests/helpers/test_proto_helpers.py | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/docs/migrations/v1_0/README.md b/docs/migrations/v1_0/README.md index 0de221aca..a1af8e7e4 100644 --- a/docs/migrations/v1_0/README.md +++ b/docs/migrations/v1_0/README.md @@ -99,6 +99,7 @@ Constructing messages is simplified in v1.0. The old API required wrapping conte **Note**: * When using `File (bytes)` in v1.0, data serialization (via base64 encoding) is not required because A2A now uses Protobuf, which handles it automatically. * In v1.0, `Part.DataPart.data` is renamed to `Part.data` and is of type `google.protobuf.Value`. Use `ParseDict` to convert a Python dict into a suitable value. See the examples below for more details. +* `google.protobuf.Value` stores every number as a double, so whole numbers in a data Part are read back as floats on every transport (`4326` becomes `4326.0`). This is a fidelity loss, not a spec violation, since JSON does not distinguish the two. On the receiving side, narrow floats for fields you model as integers (for example with `float.is_integer()`) and leave other fields alone. **Before (v0.3):** ```python diff --git a/src/a2a/helpers/proto_helpers.py b/src/a2a/helpers/proto_helpers.py index d7c3ab8cb..bcfded761 100644 --- a/src/a2a/helpers/proto_helpers.py +++ b/src/a2a/helpers/proto_helpers.py @@ -339,6 +339,11 @@ def new_data_part( ) -> Part: """Creates a Part with structured data (google.protobuf.Value). + Note: + ``google.protobuf.Value`` stores every number as a double, so whole + numbers lose their integer type here and are read back as floats + (``4326`` becomes ``4326.0``). See ``get_data_parts``. + Args: data: JSON-serializable data to embed (dict, list, str, etc.). media_type: Optional MIME type of the part content (e.g., "text/plain", "application/json", "image/png"). @@ -407,6 +412,11 @@ def get_data_parts(parts: Sequence[Part]) -> list[Any]: Each returned element is the Python object obtained by converting the ``google.protobuf.Value`` back via ``MessageToDict``. + Note: + Whole numbers come back as floats (``4326`` becomes ``4326.0``) + because ``google.protobuf.Value`` stores numbers as doubles. For + fields you model as integers, narrow floats with ``is_integer()``. + Args: parts: A sequence of ``Part`` objects. diff --git a/tests/helpers/test_proto_helpers.py b/tests/helpers/test_proto_helpers.py index f8479aa76..0dbd6f230 100644 --- a/tests/helpers/test_proto_helpers.py +++ b/tests/helpers/test_proto_helpers.py @@ -471,6 +471,14 @@ def test_get_data_parts_empty() -> None: assert get_data_parts(parts) == [] +def test_get_data_parts_returns_whole_numbers_as_floats() -> None: + """Pins the documented behavior: Value stores numbers as doubles.""" + result = get_data_parts([new_data_part({'id': 4326, 'items': [240]})]) + assert result == [{'id': 4326.0, 'items': [240.0]}] + assert isinstance(result[0]['id'], float) + assert isinstance(result[0]['items'][0], float) + + def test_get_raw_parts() -> None: parts = [ Part(raw=b'\x89PNG'), From 99810b4fd01ee6c75191d6b553d5b6a371bf58cc Mon Sep 17 00:00:00 2001 From: Carsten Piepel Date: Fri, 23 Oct 2026 10:18:35 -0400 Subject: [PATCH 2/2] test(helpers): expect integers to survive a data Part round trip, xfail strict Review feedback: a passing test that asserts int becomes float would record the conversion as intended. Assert the desired behavior instead and mark it strict xfail, so CI flags the moment it starts passing. Co-Authored-By: Claude Fable 5.1 --- tests/helpers/test_proto_helpers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/helpers/test_proto_helpers.py b/tests/helpers/test_proto_helpers.py index 0dbd6f230..a3080a7d7 100644 --- a/tests/helpers/test_proto_helpers.py +++ b/tests/helpers/test_proto_helpers.py @@ -471,12 +471,15 @@ def test_get_data_parts_empty() -> None: assert get_data_parts(parts) == [] -def test_get_data_parts_returns_whole_numbers_as_floats() -> None: - """Pins the documented behavior: Value stores numbers as doubles.""" +@pytest.mark.xfail( + reason='https://github.com/a2aproject/a2a-python/issues/1328', + strict=True, +) +def test_get_data_parts_keeps_whole_numbers_as_ints() -> None: result = get_data_parts([new_data_part({'id': 4326, 'items': [240]})]) - assert result == [{'id': 4326.0, 'items': [240.0]}] - assert isinstance(result[0]['id'], float) - assert isinstance(result[0]['items'][0], float) + assert result == [{'id': 4326, 'items': [240]}] + assert isinstance(result[0]['id'], int) + assert isinstance(result[0]['items'][0], int) def test_get_raw_parts() -> None: