Skip to content

Commit 654774f

Browse files
carstenpiepelclaudeIwaniukooo11
authored
docs(helpers): note that whole numbers in a data Part come back as floats (#1373)
# Description Whole numbers placed in a data Part come back as floats. For example, `4326` becomes `4326.0`. This happens because `google.protobuf.Value` stores every number as a double. It affects every transport and is not a spec violation, since JSON does not distinguish the two. As agreed in #1328, this PR documents the behavior and does not change it. - Add a short note to the docstrings of `new_data_part` and `get_data_parts`. - Add one bullet to the v1.0 migration guide with the client-side workaround. - Add a strict xfail test that expects integers to stay integers, so CI flags it when the behavior changes. - [x] Follow the [`CONTRIBUTING` Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md). - [x] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - [x] Ensure the tests and linter pass (Run `bash scripts/format.sh` from the repository root to format) - [x] Appropriate docs were updated (if necessary) Refs #1328 🦕 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Mateusz Iwaniuk <45974414+Iwaniukooo11@users.noreply.github.com>
1 parent 4b1b587 commit 654774f

3 files changed

Lines changed: 22 additions & 0 deletions

File tree

‎docs/migrations/v1_0/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Constructing messages is simplified in v1.0. The old API required wrapping conte
9999
**Note**:
100100
* When using `File (bytes)` in v1.0, data serialization (via base64 encoding) is not required because A2A now uses Protobuf, which handles it automatically.
101101
* 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.
102+
* `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.
102103

103104
**Before (v0.3):**
104105
```python

‎src/a2a/helpers/proto_helpers.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,11 @@ def new_data_part(
339339
) -> Part:
340340
"""Creates a Part with structured data (google.protobuf.Value).
341341
342+
Note:
343+
``google.protobuf.Value`` stores every number as a double, so whole
344+
numbers lose their integer type here and are read back as floats
345+
(``4326`` becomes ``4326.0``). See ``get_data_parts``.
346+
342347
Args:
343348
data: JSON-serializable data to embed (dict, list, str, etc.).
344349
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]:
407412
Each returned element is the Python object obtained by converting
408413
the ``google.protobuf.Value`` back via ``MessageToDict``.
409414
415+
Note:
416+
Whole numbers come back as floats (``4326`` becomes ``4326.0``)
417+
because ``google.protobuf.Value`` stores numbers as doubles. For
418+
fields you model as integers, narrow floats with ``is_integer()``.
419+
410420
Args:
411421
parts: A sequence of ``Part`` objects.
412422

‎tests/helpers/test_proto_helpers.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,17 @@ def test_get_data_parts_empty() -> None:
471471
assert get_data_parts(parts) == []
472472

473473

474+
@pytest.mark.xfail(
475+
reason='https://github.com/a2aproject/a2a-python/issues/1328',
476+
strict=True,
477+
)
478+
def test_get_data_parts_keeps_whole_numbers_as_ints() -> None:
479+
result = get_data_parts([new_data_part({'id': 4326, 'items': [240]})])
480+
assert result == [{'id': 4326, 'items': [240]}]
481+
assert isinstance(result[0]['id'], int)
482+
assert isinstance(result[0]['items'][0], int)
483+
484+
474485
def test_get_raw_parts() -> None:
475486
parts = [
476487
Part(raw=b'\x89PNG'),

0 commit comments

Comments
 (0)